provider/authorization.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: "authorization.aug.md" explains this file. Read it before changes; refresh with aug spec.
import AuthorizationRequest and AuthorizationCode and LoginForm and LoginError from contracts
import ProviderLogin and ProviderFailure from views
import verifyCredentials from credentials
import settings and securityHeaders and withCookie from common
import Crypto from crypto
import Clock from time
import ExpiringStore and StoreFull from memory
import urlEncode from web
/** Validate the registered client before offering a login form. A malformed redirect is never followed. */
endpoint GET "/provider/authorize" as authorize(string response_type from query, string client_id from query, string redirect_uri from query, string requestedScope from query "scope", string state from query, string nonce from query, string code_challenge from query, string code_challenge_method from query, resolve Crypto crypto, resolve Clock clock, resolve ExpiringStore<AuthorizationRequest> requests) unless LoginError with status 400 and CryptoError with status 503 and TimeError with status 503 and StoreFull with status 503 and HttpError:
config = settings()
if client_id != config.clientId or redirect_uri != config.callback or response_type != "code" or code_challenge_method != "S256":
throw LoginError()
if requestedScope != "openid" and requestedScope != "openid profile":
throw LoginError()
if not state.isToken(min=43, max=128) or not nonce.isToken(min=43, max=128) or code_challenge.length() != 43:
throw LoginError()
try:
if crypto.decodeBase64url(input=code_challenge).length() != 32:
throw LoginError()
catch CryptoError error:
throw LoginError()
requestId = crypto.random(size=32).base64url()
browser = crypto.random(size=32).base64url()
csrf = crypto.random(size=32).base64url()
now = clock.now()
request = AuthorizationRequest(
clientId=client_id,
redirectUri=redirect_uri,
state=state,
nonce=nonce,
challenge=code_challenge,
browser=browser,
csrf=csrf,
expires=now + 300
)
requests.put(key=requestId, value=request, expires=request.expires, now=now)
headers = withCookie(
headers=securityHeaders(),
name="aug_authorize",
value=browser,
path="/provider",
maxAge=300,
secure=config.secureCookies
)
return HttpResponse(
body=ProviderLogin(
requestId,
csrf,
message="Authorize the registered August login app.",
submit=handle providerLogin(input from form)
),
headers=headers
)
/** The browser binding and CSRF token are checked before credentials. Each form request is consumed once. */
endpoint POST "/provider/login" as providerLogin(LoginForm form from form, optional string browser from cookie "aug_authorize", optional string origin from header "origin", resolve Crypto crypto, resolve Clock clock, resolve ExpiringStore<AuthorizationRequest> requests, resolve ExpiringStore<AuthorizationCode> codes) unless CryptoError with status 503 and TimeError with status 503 and StoreFull with status 503 and HttpError:
config = settings()
try:
if origin != config.baseUrl:
return HttpResponse(
body=ProviderFailure(message="The sign-in form must come from this app."),
status=403,
headers=securityHeaders()
)
match requests.take(key=form.request_id, now=clock.now()):
when null:
return HttpResponse(
body=ProviderFailure(
message="The sign-in request expired or was already used."
),
status=400,
headers=securityHeaders()
)
when some request:
match browser:
when null:
return HttpResponse(
body=ProviderFailure(
message="The browser binding is missing."
),
status=403,
headers=securityHeaders()
)
when some secret:
if not crypto.equal(
left=secret.bytes(),
right=request.browser.bytes()
) or not crypto.equal(
left=form.csrf.bytes(),
right=request.csrf.bytes()
):
return HttpResponse(
body=ProviderFailure(
message="The sign-in form could not be verified."
),
status=403,
headers=securityHeaders()
)
if not verifyCredentials(
username=form.username,
password=form.password
):
return HttpResponse(
body=ProviderFailure(
message="The username or password was not accepted."
),
status=401,
headers=securityHeaders()
)
now = clock.now()
code = crypto.random(size=32).base64url()
grant = AuthorizationCode(
clientId=request.clientId,
redirectUri=request.redirectUri,
challenge=request.challenge,
nonce=request.nonce,
subject="demo-ada",
name="Ada",
expires=now + 60
)
codes.put(key=code, value=grant, expires=grant.expires, now=now)
location = request.redirectUri + "?code=" + urlEncode(input=code) + "&state=" + urlEncode(input=request.state)
headers = withCookie(
headers=securityHeaders().with(name="location", value=location),
name="aug_authorize",
value="",
path="/provider",
maxAge=0,
secure=config.secureCookies
)
return HttpResponse(
body=<p>Returning to the application.</p>,
status=303,
headers=headers
)
catch HttpError error:
return HttpResponse(
body=ProviderFailure(message="The submitted form is invalid."),
status=400,
headers=securityHeaders()
)// aug-spec: "authorization.aug.md" explains this file. Read it before changes; refresh with aug spec.
import AuthorizationRequest and AuthorizationCode and LoginForm and LoginError from contracts
import ProviderLogin and ProviderFailure from views
import verifyCredentials from credentials
import settings and securityHeaders and withCookie from common
import Crypto from crypto
import Clock from time
import ExpiringStore and StoreFull from memory
import urlEncode from web
/** Validate the registered client before offering a login form. A malformed redirect is never followed. */
endpoint GET "/provider/authorize" as authorize(string response_type from query, string client_id from query, string redirect_uri from query, string requestedScope from query "scope", string state from query, string nonce from query, string code_challenge from query, string code_challenge_method from query, resolve Crypto crypto, resolve Clock clock, resolve ExpiringStore<AuthorizationRequest> requests) unless LoginError with status 400 and CryptoError with status 503 and TimeError with status 503 and StoreFull with status 503 and HttpError {
config = settings()
if client_id != config.clientId or redirect_uri != config.callback or response_type != "code" or code_challenge_method != "S256" {
throw LoginError()
}
if requestedScope != "openid" and requestedScope != "openid profile" {
throw LoginError()
}
if not state.isToken(min=43, max=128) or not nonce.isToken(min=43, max=128) or code_challenge.length() != 43 {
throw LoginError()
}
try {
if crypto.decodeBase64url(input=code_challenge).length() != 32 {
throw LoginError()
}
}
catch CryptoError error {
throw LoginError()
}
requestId = crypto.random(size=32).base64url()
browser = crypto.random(size=32).base64url()
csrf = crypto.random(size=32).base64url()
now = clock.now()
request = AuthorizationRequest(
clientId=client_id,
redirectUri=redirect_uri,
state=state,
nonce=nonce,
challenge=code_challenge,
browser=browser,
csrf=csrf,
expires=now + 300
)
requests.put(key=requestId, value=request, expires=request.expires, now=now)
headers = withCookie(
headers=securityHeaders(),
name="aug_authorize",
value=browser,
path="/provider",
maxAge=300,
secure=config.secureCookies
)
return HttpResponse(
body=ProviderLogin(
requestId,
csrf,
message="Authorize the registered August login app.",
submit=handle providerLogin(input from form)
),
headers=headers
)
}
/** The browser binding and CSRF token are checked before credentials. Each form request is consumed once. */
endpoint POST "/provider/login" as providerLogin(LoginForm form from form, optional string browser from cookie "aug_authorize", optional string origin from header "origin", resolve Crypto crypto, resolve Clock clock, resolve ExpiringStore<AuthorizationRequest> requests, resolve ExpiringStore<AuthorizationCode> codes) unless CryptoError with status 503 and TimeError with status 503 and StoreFull with status 503 and HttpError {
config = settings()
try {
if origin != config.baseUrl {
return HttpResponse(
body=ProviderFailure(message="The sign-in form must come from this app."),
status=403,
headers=securityHeaders()
)
}
match requests.take(key=form.request_id, now=clock.now()) {
when null {
return HttpResponse(
body=ProviderFailure(
message="The sign-in request expired or was already used."
),
status=400,
headers=securityHeaders()
)
}
when some request {
match browser {
when null {
return HttpResponse(
body=ProviderFailure(
message="The browser binding is missing."
),
status=403,
headers=securityHeaders()
)
}
when some secret {
if not crypto.equal(
left=secret.bytes(),
right=request.browser.bytes()
) or not crypto.equal(
left=form.csrf.bytes(),
right=request.csrf.bytes()
) {
return HttpResponse(
body=ProviderFailure(
message="The sign-in form could not be verified."
),
status=403,
headers=securityHeaders()
)
}
}
}
if not verifyCredentials(
username=form.username,
password=form.password
) {
return HttpResponse(
body=ProviderFailure(
message="The username or password was not accepted."
),
status=401,
headers=securityHeaders()
)
}
now = clock.now()
code = crypto.random(size=32).base64url()
grant = AuthorizationCode(
clientId=request.clientId,
redirectUri=request.redirectUri,
challenge=request.challenge,
nonce=request.nonce,
subject="demo-ada",
name="Ada",
expires=now + 60
)
codes.put(key=code, value=grant, expires=grant.expires, now=now)
location = request.redirectUri + "?code=" + urlEncode(input=code) + "&state=" + urlEncode(input=request.state)
headers = withCookie(
headers=securityHeaders().with(name="location", value=location),
name="aug_authorize",
value="",
path="/provider",
maxAge=0,
secure=config.secureCookies
)
return HttpResponse(
body=<p>Returning to the application.</p>,
status=303,
headers=headers
)
}
}
}
catch HttpError error {
return HttpResponse(
body=ProviderFailure(message="The submitted form is invalid."),
status=400,
headers=securityHeaders()
)
}
}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.
authorize · source
authorize handles GET /provider/authorize. Validate the registered client before offering a login form. A malformed redirect is never followed.
It reads response_type, client_id, redirect_uri, scope, state, nonce, code_challenge, and code_challenge_method from the HTTP query. scope is called requestedScope here. It gets crypto (Crypto), clock (Clock), and requests (ExpiringStore<AuthorizationRequest>) from dependency injection. It can call Crypto.decodeBase64url, Crypto.random, Clock.now, and ExpiringStore<AuthorizationRequest>.put.
The handler responds with HTTP 400 for LoginError, HTTP 503 for CryptoError, HTTP 503 for TimeError, and HTTP 503 for StoreFull. It can also raise HttpError.
It gets config from settings. It checks that client_id equals config.clientId and redirect_uri equals config.callback and response_type equals "code" and code_challenge_method equals "S256", requestedScope is either "openid" or "openid profile", and state is a URL-safe ASCII token with 43 to 128 characters and nonce is a URL-safe ASCII token with 43 to 128 characters and the byte length of code_challenge equals 43. It raises a LoginError at the first failed check. source
It checks that the byte length of crypto.decodeBase64url with input from code_challenge equals 32. It raises a LoginError at the first failed check. If this work raises CryptoError, it raises a LoginError. It sets requestId, browser, and csrf separately, each to the URL-safe base64 encoding of crypto.random with size 32. source
It sets now to clock.now. It sets request to an AuthorizationRequest with clientId from client_id, redirectUri from redirect_uri, state, nonce, challenge from code_challenge, browser, csrf, and expires from now plus 300. It calls requests.put with key from requestId, value from request, request.expires, and now. It sets headers to withCookie with headers from securityHeaders, name "aug_authorize", value from browser, path "/provider", maxAge 300, and secure from config.secureCookies. source
It returns HTTP 200 with ProviderLogin with requestId, csrf, message "Authorize the registered August login app.", and submit from a form action that sends POST /provider/login to providerLogin on submission and headers headers. source
Checked interface
authorize(string response_type, string client_id, string redirect_uri, string requestedScope, string state, string nonce, string code_challenge, string code_challenge_method, resolve Crypto crypto, resolve Clock clock, resolve ExpiringStore<AuthorizationRequest> requests) returns HttpResponse<Html> unless CryptoError and HttpError and LoginError and StoreFull and TimeError uses Crypto.decodeBase64url, Crypto.random, Clock.now, ExpiringStore<AuthorizationRequest>.putIt reads response_type, client_id, redirect_uri, scope, state, nonce, code_challenge, and code_challenge_method from the HTTP query. scope is called requestedScope here. It gets crypto (Crypto), clock (Clock), and requests (ExpiringStore<AuthorizationRequest>) from dependency injection. It can call Crypto.decodeBase64url, Crypto.random, Clock.now, and ExpiringStore<AuthorizationRequest>.put.
The handler responds with HTTP 400 for LoginError, HTTP 503 for CryptoError, HTTP 503 for TimeError, and HTTP 503 for StoreFull. It can also raise HttpError.
providerLogin · source
providerLogin handles POST /provider/login. The browser binding and CSRF token are checked before credentials. Each form request is consumed once.
It takes form as LoginForm from the HTTP form, browser as optional string from the HTTP cookie aug_authorize, and origin as optional string from the HTTP header origin. It gets crypto (Crypto), clock (Clock), requests (ExpiringStore<AuthorizationRequest>), and codes (ExpiringStore<AuthorizationCode>) from dependency injection. Omitted optional inputs are null. It can call Clock.now, ExpiringStore<AuthorizationRequest>.take, Crypto.equal, Crypto.random, ExpiringStore<AuthorizationCode>.put, Crypto.passwordHash, and Crypto.decodeBase64url.
The handler responds with HTTP 503 for CryptoError, HTTP 503 for TimeError, and HTTP 503 for StoreFull. It can also raise HttpError.
It gets config from settings. If origin does not equal config.baseUrl, it returns HTTP 403 with ProviderFailure showing "The sign-in form must come from this app." and securityHeaders headers. It obtains requests.take with key from form.request_id and now from clock.now. If no value is found, it returns HTTP 400 with ProviderFailure showing "The sign-in request expired or was already used." and securityHeaders headers. source
The non-null result becomes request. If browser is null, it returns HTTP 403 with ProviderFailure showing "The browser binding is missing." and securityHeaders headers. The non-null browser becomes secret. If crypto.equal with left from the UTF-8 bytes of secret and right from the UTF-8 bytes of request.browser returns false or crypto.equal with left from the UTF-8 bytes of form.csrf and right from the UTF-8 bytes of request.csrf returns false, it returns HTTP 403 with ProviderFailure showing "The sign-in form could not be verified." and securityHeaders headers. source
If verifyCredentials with form.username and form.password using injected crypto returns false, it returns HTTP 401 with ProviderFailure showing "The username or password was not accepted." and securityHeaders headers. It sets now to clock.now. It sets code to the URL-safe base64 encoding of crypto.random with size 32. It sets grant to an AuthorizationCode with request.clientId, request.redirectUri, request.challenge, request.nonce, subject "demo-ada", name "Ada", and expires from now plus 60. source
It calls codes.put with key from code, value from grant, grant.expires, and now. It builds location as the text {request.redirectUri}?code={urlEncode with input from code}&state={urlEncode with input from request.state}. It sets headers to withCookie with headers from securityHeaders with the header "location" set to location, name "aug_authorize", value "", path "/provider", maxAge 0, and secure from config.secureCookies. It returns HTTP 303 with a paragraph containing Returning to the application. with escaped text and headers headers. source
If this work raises HttpError, it returns HTTP 400 with ProviderFailure showing "The submitted form is invalid." and securityHeaders headers. source
Checked interface
providerLogin(LoginForm form, optional string browser, optional string origin, resolve Crypto crypto, resolve Clock clock, resolve ExpiringStore<AuthorizationRequest> requests, resolve ExpiringStore<AuthorizationCode> codes) returns HttpResponse<Html> unless CryptoError and HttpError and StoreFull and TimeError uses Clock.now, ExpiringStore<AuthorizationRequest>.take, Crypto.equal, Crypto.random, ExpiringStore<AuthorizationCode>.put, Crypto.passwordHash, Crypto.decodeBase64urlIt takes form as LoginForm from the HTTP form, browser as optional string from the HTTP cookie aug_authorize, and origin as optional string from the HTTP header origin. It gets crypto (Crypto), clock (Clock), requests (ExpiringStore<AuthorizationRequest>), and codes (ExpiringStore<AuthorizationCode>) from dependency injection. Omitted optional inputs are null. It can call Clock.now, ExpiringStore<AuthorizationRequest>.take, Crypto.equal, Crypto.random, ExpiringStore<AuthorizationCode>.put, Crypto.passwordHash, and Crypto.decodeBase64url.
The handler responds with HTTP 503 for CryptoError, HTTP 503 for TimeError, and HTTP 503 for StoreFull. It can also raise HttpError.
Dependencies
It uses ExpiringStore (put and take) and StoreFull from memory. It uses urlEncode from web. It uses Crypto (decodeBase64url, equal, passwordHash, and random) from crypto. It uses Clock (now) from time.
It uses securityHeaders, withCookie, and settings from common. It uses Settings (baseUrl, callback, clientId, and secureCookies). It uses AuthorizationCode (expires), AuthorizationRequest (browser, challenge, clientId, csrf, expires, nonce, redirectUri, and state), LoginError, and LoginForm (csrf, password, request_id, and username) from contracts. It uses verifyCredentials from credentials.
It uses ProviderFailure and ProviderLogin from views.
Built-in operations follow the language reference.