Skip to content

External Authentication

Tom Udding edited this page Aug 20, 2026 · 1 revision

Authentication API

The authentication API for the GEWIS website lets external applications authenticate users against their GEWIS membership. It issues JSON Web Tokens signed with one of the association's asymmetric keys, and publishes the corresponding public keys through a JWKS endpoint.

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174 when, and only when, they appear in all capitals.

Important

This page describes the flow as of GEWISWEB-ng. The following are deprecated and will all be removed before the end of 2026:

  • The HS512 signature profile, and with it the per-application shared secret.
  • The RS512 signature profile.
  • Delivery of the token as a query parameter.
  • The lidnr claim.

These are removed together, because they describe one legacy profile rather than four independent settings. See Migrating from the legacy profile.

Apps

An application is registered with the association and has the following properties. The signature profile and the token delivery method are configured per application, so what an application receives is determined by its registration rather than by anything it sends.

Property Visibility Description
appId Public Identifies the application in the authorisation URL.
Signature Public Profile used to sign the token. Defaults to EdDSA. See Signature profiles.
Token delivery Public Whether the token is returned in the URL fragment or as a query parameter. Defaults to the fragment.
Secret Shared Only set for applications using the HS512 profile. Null for every other profile.
callback Public URL the user is redirected to after successful authorisation.
url Public URL the user is redirected to when they decline.
Claims Configured Which optional claims the token carries. See Optional claims.
Enabled Internal Whether the application may currently be used to authenticate.
Expires at Internal Moment after which the application may no longer be used to authenticate, if set.

Both callback and url MUST be served over TLS, so both MUST start with https://.

An application only mints tokens while it is enabled and, if an expiry moment is set, that moment has not passed. Registrations MAY be given an expiry when they are issued for a limited purpose, so an application that works today can stop working on a known date. Maintainers SHOULD know whether their registration carries an expiry, and SHOULD NOT assume that a sudden inability to authenticate is a fault on their own side.

Signature profiles

Profile kty Parameters Defined in Status
EdDSA OKP crv: Ed25519 RFC 8037 Default for new registrations
ES512 EC crv: P-521 RFC 7518, section 3.4 Supported
PS512 RSA RSASSA-PSS with SHA-512, MGF1 with SHA-512 RFC 7518, section 3.5 Supported
RS512 RSA RSASSA-PKCS1-v1_5 with SHA-512 RFC 7518, section 3.3 Deprecated, removed before the end of 2026
HS512 n/a HMAC with SHA-512, keyed with the application's shared secret RFC 7518, section 3.2 Deprecated, removed before the end of 2026

The profiles are listed in order of preference. New registrations SHOULD use EdDSA, and SHOULD only be given ES512 or PS512 where the application's platform or JWT library lacks Ed25519 support. Registrations MUST NOT be created with RS512 or HS512.

HS512 differs from the others in more than the algorithm. It is the only profile that signs with a secret shared between the association and the application, which means either party can mint a token the other will accept, and it is the only profile whose tokens do not carry a sub claim. See Subject.

Token delivery

Delivery Callback shape Status
Fragment {callback}#token={token} Default for new registrations
Query {callback}?token={token} Deprecated, removed before the end of 2026

Important

Fragment delivery exists because fragments are not sent to the server as part of the HTTP request. A token delivered this way stays out of server access logs, reverse proxy logs, browser caches and Referer headers, all of which see a query string. The token therefore has to be read client side, and the application is responsible for clearing it from the URL afterwards.

Basic flow

  1. The user requests login from the application.
  2. The application generates a nonce and stores it in the user's session. See Nonce.
  3. The application redirects the user to https://gewis.nl/token/{appId}?nonce={nonce}.
  4. The user logs in on the GEWIS website, if they are not logged in already.
  5. On first authentication the user is asked to authorise the application. If more than 90 days have passed since the last authorisation, they are reminded instead.
  6. If the user authorises the application, the GEWIS website redirects to {callback}#token={token}.
  7. The application reads the token from the fragment and verifies it. See Verifying a token.

If the user does not authorise the application, the GEWIS website redirects to {url} without a token.

sequenceDiagram
    autonumber
    actor U as User
    participant A as App
    participant G as GEWIS website

    U->>A: Authentication request
    A->>A: Generate nonce, store in user's session
    A->>U: Redirect to /token/{appId}?nonce={nonce}
    U->>G: Request token

    opt User is not logged in
        G->>U: Login form
        U->>G: Credentials
        G->>G: Verify credentials
    end

    G->>G: Check application is enabled and not expired

    alt First time authenticating
        G->>U: Authorisation prompt
        alt User confirms
            G->>U: Redirect to {callback}#token={token}
        else User denies
            G->>U: Redirect to {url}
        end
    else More than 90 days since last authorisation
        G->>U: Authorisation reminder
        U->>G: Confirms
        G->>U: Redirect to {callback}#token={token}
    else
        G->>U: Redirect to {callback}#token={token}
    end

    U->>A: Visit callback URL
    Note over U,A: The fragment stays in the browser and<br/>is not sent to the application's server

    opt Key set not cached, or unknown kid
        A->>G: GET /.well-known/jwks.json
        G->>A: Key set
    end

    A->>A: Verify signature, claims and nonce
    A->>U: Application session
Loading

Token

Tokens are JWTs (RFC 7519) using the JWS Compact Serialization (RFC 7515).

Header

Parameter Description
alg The profile the application is registered for. See Signature profiles.
kid Identifies the key in the key set that was used to sign the token. Not present for HS512.
typ JWT.

Registered claims

Claim Type Description
sub string Membership number of the GEWIS member. Not present for HS512. See Subject.
iat number Time the token was issued, in UTC.
exp number Time the token expires, in UTC. Tokens are valid for 5 minutes.
nonce string The nonce supplied by the application. Only present if the application supplied one.

Subject

Applications using an asymmetric profile receive the membership number in sub. It is present on every token those applications receive and does not have to be requested.

Warning

sub is a string, per RFC 7519, section 4.1.2, whereas the deprecated lidnr claim is a number. Applications comparing either against a membership number held locally need an explicit conversion, and MUST NOT rely on a loose comparison.

Applications using HS512 do not receive sub, and identify the member through the lidnr claim, which they MUST request explicitly. This is the only situation in which lidnr SHOULD be used. It is removed together with HS512.

Optional claims

Which further claims a token carries is configured per application. Applications MUST NOT depend on a claim they have not been granted, and SHOULD treat any granted claim as possibly null, since not every member has a value for every field.

Claim Type Description
email string or null E-mail address. Null if the member has none on record.
email_verified boolean Whether the member has an e-mail address on record. See below.
family_name string Surname or last name.
given_name string Given name or first name.
is_18_plus boolean Whether the member has reached the age of 18.
is_member boolean Whether the member is currently a statutory member and their membership has not expired.
lidnr number Membership number. Deprecated, see Subject.
membership_type string Type of membership.
middle_name string or null Middle name. Null if the member has none.
name string Full name, as the association renders it.

Note

The association only holds e-mail addresses that have been verified, so any address delivered in email is a verified one and email_verified is true whenever an address is present. A false value therefore means that no address is on record, and not that an unverified address exists. Applications MUST treat email_verified as false when the claim is absent, rather than assuming a default of true.

Use of any optional claim has to be motivated to, and approved by, the GEWIS privacy responsible. Applications SHOULD request the smallest set of claims that lets them function.

Tip

is_member is usually the right claim for deciding whether someone may use an application, rather than inferring it from the presence of a token. Authentication succeeds for anyone with a GEWIS account, including former members whose membership has expired.

Nonce

The GEWIS website accepts a nonce query parameter on the authorisation URL and, if one is supplied, includes it unchanged as the nonce claim in the issued token. If no nonce is supplied, no nonce claim is present.

Caution

Supplying and validating a nonce is not required by this API, but it is strongly RECOMMENDED. Without it an application is open to login CSRF: an attacker completes the flow for their own account, obtains a valid token, and then gets a victim to load {callback}#token={attacker token}. The victim ends up logged into the attacker's account without any indication that this happened, and anything they do afterwards, including entering personal data or spending money, happens under the attacker's account. Implementers who choose not to use a nonce SHOULD record why this is acceptable for their application.

An application that uses a nonce:

  • MUST generate it with a cryptographically secure random number generator, for example crypto.randomUUID() or crypto.getRandomValues(). Math.random() and similar MUST NOT be used.
  • MUST store it scoped to the individual user session, for example in sessionStorage on the application's own origin, or in a server side session keyed by an HttpOnly cookie.
  • MUST NOT store it in any location shared between sessions, such as a module level variable or a server side collection of outstanding nonces that is not bound to a session. If any browser can match any nonce, the check provides no protection.
  • MUST reject a token whose nonce claim is absent, or whose nonce claim does not match the value stored for that session.
  • MUST discard the stored nonce as soon as a token has been checked against it, whether or not verification succeeded. A nonce that survives a successful login can be matched by a token injected later.

Note

Rejecting tokens without a nonce claim is what makes this work. An application that compares the nonce only when the claim happens to be present is bypassed by leaving the parameter off the authorisation URL, since the resulting token is otherwise entirely valid.

A user who refreshes or navigates back after a completed login arrives at the callback with a token whose nonce has already been consumed, and will be rejected. This is intended. Applications SHOULD present this as a request to log in again rather than as an error, and SHOULD use the same response for every rejection reason so that the failure does not reveal which check failed.

Tip

Do not retry the authorisation request unconditionally after a rejection. If session storage is unavailable, which happens with some privacy extensions and browser configurations, an unconditional retry sends the user round the flow indefinitely without ever showing them what went wrong. Allow at most one automatic retry.

Key set

Public keys are published at https://gewis.nl/.well-known/jwks.json as a JWK Set (RFC 7517, RFC 8615). Every key carries kid, alg and use: "sig". The set covers the asymmetric profiles only; there is nothing to publish for HS512.

Applications SHOULD cache the key set and refresh it periodically, and SHOULD refresh it when a token presents a kid that is not in the cache, so that key rotation does not require a redeployment. Refreshes triggered by an unknown kid MUST be rate limited, since otherwise an attacker can force a request to the JWKS endpoint for every token they submit.

Verifying a token

An application MUST perform all of the following before accepting a token:

  1. Read the token from the callback URL. For fragment delivery, remove it from the URL immediately, for example with history.replaceState(), so that it is not retained in browser history.
  2. Select the verification key by matching the kid in the token header against the key set.
  3. Verify the signature.
  4. Verify that the algorithm is the one the application is registered for, using a value held in the application's own configuration. See the warning below.
  5. Verify that exp is in the future and that iat is not in the future. Clock skew tolerance SHOULD be no more than 60 seconds.
  6. Verify the nonce, if the application supplies one. See Nonce.

Warning

The accepted algorithm MUST come from the application's own configuration, and MUST NOT be taken from the alg header of the token being verified. Accepting whatever the token asks for allows an attacker to present a token signed with none, or to submit an HS512 token whose key is the RSA public key from the JWKS, which is public by definition. Both are described in RFC 8725, sections 2.1 and 3.1. Most JWT libraries require the caller to pass the permitted algorithms explicitly for this reason.

Because each application is registered for exactly one profile, this configuration SHOULD name exactly one algorithm, and MUST be updated as part of any move to a different profile. An application migrating away from HS512 MAY accept both briefly, but SHOULD NOT leave that in place once migration is complete.

Where verification happens is a design decision worth making deliberately. If the application's backend issues the session, the frontend has to pass both the token and the stored nonce to the backend, and the backend performs steps 2 through 6. Verifying only in the frontend leaves the backend accepting any correctly signed GEWIS token that is posted to it directly.

Migrating from the legacy profile

Legacy profile Current profile
HS512, keyed with the shared secret EdDSA, ES512 or PS512, verified against the JWKS
Token returned as {callback}?token={token} Token returned as {callback}#token={token}
Membership number in the opt-in lidnr claim Membership number in sub, present on every token
Nonce accepted but generally unused Nonce supplied by the application and validated on return

Both profiles are supported during the transition. The legacy profile is removed before the end of 2026, after which applications still verifying against the shared secret will stop being able to log users in.

Migration is a change of registration, not only of application code, so it has to be arranged with the association. Applications SHOULD switch to reading sub before the switchover rather than after, since sub appears as soon as the registration moves to an asymmetric profile, and SHOULD remove the shared secret from their configuration once migration is complete.

Clone this wiki locally