Auth

Specification

We use the Authorization Code Flow according to RFC6749 Section 4.1, as recommended by Internet Draft OAuth 2.0 for Browser-Based Apps, since the frontend application can be recognized as a Javascript Application without a Backend (Section 6.3 of the latter document). This might be confusing as we do have a backend, but backend in that context means a frontend backend that dynamically serves HTML, while our frontend is hosted as static HTML on GitHub Pages.

We comply fully with OAuth 2.1 and implement an Authorization Code Flow with PKCE. We comply as much as possible with OpenID Connect, except on some points that are only for interoperability (like supporting certain algorithms), which we do not require. Our compliance with OpenID Connect is not as rigorously checked, as it is a more complex standard. We used it more as a guide.

Useful resources

  • https://auth0.com/docs/security/tokens/refresh-tokens/refresh-token-rotation
    • Other pages from https://auth0.com/docs
  • https://www.ietf.org/archive/id/draft-ietf-oauth-v2-1-04.html
  • https://openid.net/specs/openid-connect-core-1_0.html
  • https://www.oauth.com/

Protocol

Resource Owner - end-user

Resource Server - dodekabackend=apiserver (Server)

  • identifier: dodekabackend_client

Client - dodekaweb (Pages)

  • identifier: dodekaweb_client
  • redirect_uri: .../auth/callback

Authorization Server (AS) = OpenID Provider (OP) - dodekabackend=apiserver (Server)

The Relying Party (RP) in the context of OpenID Connect is the Client.

  1. a

Create an Authorization Request (Section 4.1.1)

  1. (Client) AuthRedirect.tsx generates the AuthRequest:
  {
    "response_type": "code",
    "client_id":  "dodekaweb_client",
    "redirect_uri":  ".../auth/callback",
    "state": "a) STATE",
    "code_challenge": "b) CHALLENGE",
    "code_challenge_method": "S256",
    "nonce": "c) NONCE"
  }
  • a) STATE: The state is a randomly generated string that is used to later check the response
  • b) CHALLENGE: First a code verifier is generated, which is cryptographically random. A SHA256 hash is then computed (as indicated by 'S256'). The verifier is stored locally. Used as a check by the server.
  • c) NONCE (OpenID): Randomly generated used to later verify the OpenID ID token. The random value is stored, the hash is sent.
  1. (Client) The AuthRequest is encoded as an urlencoded param string, not JSON. The user is redirected to the AS with those params, specifically to ../oauth/authorize
  2. (AS) The Authorization Server (AS) validates the AuthRequest, in particular the response type (only "code"), the client_id, the redirect_uri and the format of the state, challenge and nonce. It generates a random identifier (the Flow ID). It uses this identifier to persist the process on the server by the storing the entire AuthRequest as a JSON. This is needed in order to later check everything.
  3. (AS) The user is redirected to a new page. In a perfect world, this would be a page served by the server, but in this case this is a page on the Client (../auth/credentials). The Flow ID is sent as a query parameter.
  4. (AS/Client) The next step does not fall under the OAuth protocol, as any AS is free to choose its own authentication implementation. In this case we use the OPAQUE protocol. A user supplies their username and password, which is then used with the ../auth/login/start and ../auth/login/finish endpoints to ensure the password is correct.
  5. (AS) In the final ../auth/login/finish step, the user also supplies the Flow ID. The authorization code (OPAQUE "session key") which is computed is used as a key for storing the combination of the Flow ID, username and authentication time. This is stored only for a short time.
  6. (AS/Client) Finally the user is redirected to the ../oauth/callback endpoint, again with the Flow ID but now also with the generated authorization code. This is separated from the ../auth/login/finish to neatly distinguish the steps belonging to the OAuth protocol and the selected authentication protocol. Here, the user is redirected using the state from the AuthRequest stored on the server and authorization code to the redirect_uri supplied in the initial request.
  7. (Client) At the redirect_uri (../auth/callback), the client checks their stored state with the state supplied in the redirect. If it doesn't match, the login aborts.
  8. (Client) Now, a TokenRequest is made:
  {
    "client_id":  "dodekaweb_client",
    "grant_type": "authorization_code",
    "redirect_uri":  ".../auth/callback",
    "code": "a) CODE",
    "code_verifier": "b) VERIFIER"
  }
  • a) CODE: The code that the user generated in the final authentication step (OPAQUE session key).
  • b) VERIFIER: The unhashed original cryptographically random string generated by the client for the original AuthRequest
  1. (Client) This time, it is encoded as JSON and sent as a post request to the ../oauth/token endpoint.
  2. (AS) The supplied authorization code is used to fetch the Flow ID

Security considerations of non-OAuth steps

OAuth does not exactly define authentication, nor how to store certain state. We make a few assumptions that determine the security of the login process:

  • The Flow ID (used to identify the OAuth AuthRequest throughout the entire authentication flow), the Auth ID (used to identify an OPAQUE login process, i.e. to retrieve server state generated in the first step for the second step) and the session key (used as the OAuth 'code', generated by OPAQUE) should all be sufficiently random and have enough entropy so that an attacker cannot guess a random value to intercept a login attempt.
  • Critically, in the time frame that these are valid (1000 seconds for Flow ID and 60 seconds for Auth ID and session key), there should not be so many requests that an attacker can randomly guess a correct value. All these values are at least 10 bytes of random information, meaning there are more than 10^24 different values. The session key is even 32 bytes, making it even more difficult to guess within 60 seconds.

https://openid.net/specs/openid-connect-core-1_0.html#IDToken

Remembering session