provider/token.aug
OpenID Connect login application · Source and specification
Files in this project
main.augclient/contracts.augclient/endpoints.augclient/export.augclient/login.augclient/logout.augclient/protocol.augclient/session.augclient/views.augcommon/export.augcommon/headers.augcommon/keys.augcommon/settings.augcommon/views.augprovider/authorization.augprovider/contracts.augprovider/credentials.augprovider/discovery.augprovider/export.augprovider/token.augprovider/userinfo.augprovider/views.aug
Code
// aug-spec: "token.aug.md" explains this file. Read it before changes; refresh with aug spec.
import AuthorizationCode and AccessGrant and TokenForm and TokenResponse and OAuthError and IdClaims from contracts
import settings and SigningKeys and KeyError and securityHeaders from common
import Crypto and signJwt and JwtError from crypto
import Clock from time
import ExpiringStore and StoreFull from memory
_oauthError(string code, string description):
return HttpResponse(
body=Json(value=OAuthError(error=code, error_description=description)),
status=400,
headers=securityHeaders()
)
/** A real OAuth token endpoint. Exact client/redirect binding, S256 PKCE, expiry and one-use codes are enforced. Errors use OAuth JSON. */
endpoint POST "/provider/token" as token(HttpRequest http from request, resolve Crypto crypto, resolve Clock clock, resolve SigningKeys keys, resolve ExpiringStore<AuthorizationCode> codes, resolve ExpiringStore<AccessGrant> access) unless CryptoError with status 503 and TimeError with status 503 and KeyError and JwtError and StoreFull with status 503 and HttpError:
config = settings()
try:
form = http.form<TokenForm>()
if form.grant_type != "authorization_code":
return _oauthError(
code="unsupported_grant_type",
description="Only authorization_code is supported."
)
if form.client_id != config.clientId:
return _oauthError(
code="invalid_client",
description="The registered client is required."
)
if not form.code.isToken(min=43, max=43) or not form.code_verifier.isToken(min=43, max=128):
return _oauthError(
code="invalid_grant",
description="The authorization grant is invalid."
)
now = clock.now()
match codes.take(key=form.code, now=now):
when null:
return _oauthError(
code="invalid_grant",
description="The authorization grant is invalid."
)
when some grant:
challenge = crypto.sha256(input=form.code_verifier.bytes()).base64url()
if grant.clientId != form.client_id or grant.redirectUri != form.redirect_uri or not crypto.equal(
left=challenge.bytes(),
right=grant.challenge.bytes()
):
return _oauthError(
code="invalid_grant",
description="The authorization grant is invalid."
)
claims = IdClaims(
iss=config.issuer,
sub=grant.subject,
aud=grant.clientId,
exp=now + 300,
iat=now,
nonce=grant.nonce,
name=grant.name
)
idToken = signJwt(
key=keys.provider(),
claims=Json(value=claims),
kid="provider-1",
tokenType="JWT"
)
accessToken = crypto.random(size=32).base64url()
value = AccessGrant(
subject=grant.subject,
name=grant.name,
expires=now + 300
)
access.put(
key=accessToken,
value=value,
expires=value.expires,
now=now
)
body = TokenResponse(
token_type="Bearer",
access_token=accessToken,
id_token=idToken,
expires_in=300,
scope="openid profile"
)
return HttpResponse(body=Json(value=body), headers=securityHeaders())
catch HttpError error:
return _oauthError(
code="invalid_request",
description="Submit the required URL-encoded token fields once each."
)// aug-spec: "token.aug.md" explains this file. Read it before changes; refresh with aug spec.
import AuthorizationCode and AccessGrant and TokenForm and TokenResponse and OAuthError and IdClaims from contracts
import settings and SigningKeys and KeyError and securityHeaders from common
import Crypto and signJwt and JwtError from crypto
import Clock from time
import ExpiringStore and StoreFull from memory
_oauthError(string code, string description) {
return HttpResponse(
body=Json(value=OAuthError(error=code, error_description=description)),
status=400,
headers=securityHeaders()
)
}
/** A real OAuth token endpoint. Exact client/redirect binding, S256 PKCE, expiry and one-use codes are enforced. Errors use OAuth JSON. */
endpoint POST "/provider/token" as token(HttpRequest http from request, resolve Crypto crypto, resolve Clock clock, resolve SigningKeys keys, resolve ExpiringStore<AuthorizationCode> codes, resolve ExpiringStore<AccessGrant> access) unless CryptoError with status 503 and TimeError with status 503 and KeyError and JwtError and StoreFull with status 503 and HttpError {
config = settings()
try {
form = http.form<TokenForm>()
if form.grant_type != "authorization_code" {
return _oauthError(
code="unsupported_grant_type",
description="Only authorization_code is supported."
)
}
if form.client_id != config.clientId {
return _oauthError(
code="invalid_client",
description="The registered client is required."
)
}
if not form.code.isToken(min=43, max=43) or not form.code_verifier.isToken(min=43, max=128) {
return _oauthError(
code="invalid_grant",
description="The authorization grant is invalid."
)
}
now = clock.now()
match codes.take(key=form.code, now=now) {
when null {
return _oauthError(
code="invalid_grant",
description="The authorization grant is invalid."
)
}
when some grant {
challenge = crypto.sha256(input=form.code_verifier.bytes()).base64url()
if grant.clientId != form.client_id or grant.redirectUri != form.redirect_uri or not crypto.equal(
left=challenge.bytes(),
right=grant.challenge.bytes()
) {
return _oauthError(
code="invalid_grant",
description="The authorization grant is invalid."
)
}
claims = IdClaims(
iss=config.issuer,
sub=grant.subject,
aud=grant.clientId,
exp=now + 300,
iat=now,
nonce=grant.nonce,
name=grant.name
)
idToken = signJwt(
key=keys.provider(),
claims=Json(value=claims),
kid="provider-1",
tokenType="JWT"
)
accessToken = crypto.random(size=32).base64url()
value = AccessGrant(
subject=grant.subject,
name=grant.name,
expires=now + 300
)
access.put(
key=accessToken,
value=value,
expires=value.expires,
now=now
)
body = TokenResponse(
token_type="Bearer",
access_token=accessToken,
id_token=idToken,
expires_in=300,
scope="openid profile"
)
return HttpResponse(body=Json(value=body), headers=securityHeaders())
}
}
}
catch HttpError error {
return _oauthError(
code="invalid_request",
description="Submit the required URL-encoded token fields once each."
)
}
}Compiled specification
Plain handler results default to HTTP 200 unless another status is declared. HttpResponse values choose their own status. Unhandled request failures return HTTP 500 and cancel the request tasks.
token · source
token handles POST /provider/token. A real OAuth token endpoint. Exact client/redirect binding, S256 PKCE, expiry and one-use codes are enforced. Errors use OAuth JSON.
It takes http as HttpRequest from the HTTP request. It gets crypto (Crypto), clock (Clock), keys (SigningKeys), codes (ExpiringStore<AuthorizationCode>), and access (ExpiringStore<AccessGrant>) from dependency injection. It can call Clock.now, ExpiringStore<AuthorizationCode>.take, Crypto.sha256, Crypto.equal, SigningKeys.provider, Crypto.random, ExpiringStore<AccessGrant>.put, and Crypto.signRsa. The handler responds with HTTP 503 for CryptoError, HTTP 503 for TimeError, and HTTP 503 for StoreFull.
It can also raise HttpError, JwtError, and KeyError.
It gets config from settings. It sets form to http.form for TokenForm. If form.grant_type does not equal "authorization_code", it returns _oauthError with code "unsupported_grant_type" and description "Only authorization_code is supported.". If form.client_id does not equal config.clientId, it returns _oauthError with code "invalid_client" and description "The registered client is required.". source
If form.code is not a URL-safe ASCII token with 43 to 43 characters or form.code_verifier is not a URL-safe ASCII token with 43 to 128 characters, it returns _oauthError with code "invalid_grant" and description "The authorization grant is invalid.". It sets now to clock.now. It obtains codes.take with key from form.code and now. If no value is found, it returns _oauthError with code "invalid_grant" and description "The authorization grant is invalid.". source
The non-null result becomes grant. It sets challenge to the URL-safe base64 encoding of crypto.sha256 with input from the UTF-8 bytes of form.code_verifier. If grant.clientId does not equal form.client_id or grant.redirectUri does not equal form.redirect_uri or crypto.equal with left from the UTF-8 bytes of challenge and right from the UTF-8 bytes of grant.challenge returns false, it returns _oauthError with code "invalid_grant" and description "The authorization grant is invalid.". It sets claims to an IdClaims with iss from config.issuer, sub from grant.subject, aud from grant.clientId, exp from now plus 300, iat from now, grant.nonce, and grant.name. source
It sets idToken to signJwt with key from keys.provider, claims from a Json with value from claims, kid "provider-1", and tokenType "JWT" using injected crypto. It sets accessToken to the URL-safe base64 encoding of crypto.random with size 32. It sets value to an AccessGrant with grant.subject, grant.name, and expires from now plus 300. It calls access.put with key from accessToken, value, value.expires, and now. source
It sets body to a TokenResponse with token_type "Bearer", access_token from accessToken, id_token from idToken, expires_in 300, and scope "openid profile". It returns HTTP 200 with a Json with value from body and securityHeaders headers. If this work raises HttpError, it returns _oauthError with code "invalid_request" and description "Submit the required URL-encoded token fields once each.". source
Checked interface
token(HttpRequest http, resolve Crypto crypto, resolve Clock clock, resolve SigningKeys keys, resolve ExpiringStore<AuthorizationCode> codes, resolve ExpiringStore<AccessGrant> access) returns HttpResponse<Json> unless CryptoError and HttpError and JwtError and KeyError and StoreFull and TimeError uses Clock.now, ExpiringStore<AuthorizationCode>.take, Crypto.sha256, Crypto.equal, SigningKeys.provider, Crypto.random, ExpiringStore<AccessGrant>.put, Crypto.signRsaIt takes http as HttpRequest from the HTTP request. It gets crypto (Crypto), clock (Clock), keys (SigningKeys), codes (ExpiringStore<AuthorizationCode>), and access (ExpiringStore<AccessGrant>) from dependency injection. It can call Clock.now, ExpiringStore<AuthorizationCode>.take, Crypto.sha256, Crypto.equal, SigningKeys.provider, Crypto.random, ExpiringStore<AccessGrant>.put, and Crypto.signRsa. The handler responds with HTTP 503 for CryptoError, HTTP 503 for TimeError, and HTTP 503 for StoreFull.
It can also raise HttpError, JwtError, and KeyError.
_oauthError · source
It is private to its defining scope. It takes code and description as strings. Failures can raise HttpError. It returns HTTP 400 with a Json with value from an OAuthError with error from code and error_description from description and securityHeaders headers. source
Checked interface
_oauthError(string code, string description) returns HttpResponse<Json> unless HttpErrorIt takes code and description as strings. Failures can raise HttpError.
Dependencies
It uses ExpiringStore (put and take) and StoreFull from memory. It uses Crypto (equal, random, sha256, and signRsa), JwtError, and signJwt from crypto. It uses Clock (now) from time. It uses securityHeaders, KeyError, SigningKeys (provider), and settings from common.
It uses Settings (clientId and issuer). It uses AccessGrant (expires), AuthorizationCode (challenge, clientId, name, nonce, redirectUri, and subject), IdClaims, OAuthError, TokenForm (client_id, code, code_verifier, grant_type, and redirect_uri), and TokenResponse from contracts.
Built-in operations follow the language reference.