Skip to main content

How does Log in with Congressus work?

The technical steps of Log in with Congressus, from login to refresh token

Once you have created an app (Add an OAuth2 app), members log in through these steps. If your app is on OpenID Connect, a standard library does this for you; see Log in with Congressus via OpenID Connect.

In the examples, https://www.vereniging.nl is your association's website.

Step 1: Send the member to your association's website

  • Create a random state and keep it, for instance in a cookie. You get it back in step 2.

  • Choose the scopes, separated by spaces, for instance openid profile email. If you name no scopes, we ask for openid profile.

  • Use PKCE: create a random code_verifier and send its SHA-256 hash as code_challenge, with code_challenge_method=S256. Required for a public app, recommended for every app.

Then send the member to:

https://www.vereniging.nl/oauth/authorize?response_type=code&client_id=<client_id>&scope=openid profile email&state=<state>&code_challenge=<code_challenge>&code_challenge_method=S256

You can also send:

  • redirect_uri: one of your app's Redirect URIs, exactly as set. Without it, we use the first.

  • nonce: a random value we put back in the ID token.

  • prompt: login makes the member log in again, consent always asks for permission, and none shows no page but returns an error when logging in or permission is needed.

  • max_age: after how many seconds since logging in the member must log in again.

Step 2: The member comes back

The member logs in if needed and gives your app permission, unless your app's settings let it skip that. We then send the member to your redirect URI, with the code and your state:

https://eetmee.vereniging.nl/oauth/return?code=4WOSENHMG4MYCMB5BGGCJXHILVATNVOPXBRODIN3WOOI44XRXLIA&state=<state>

Check that the state is the one you kept in step 1. That way you know the same member is coming back.

Something went wrong

Your redirect URI then ends in ?error=<error>&state=<state>. The error is one of:

  • unauthorized_client: your app is locked.

  • unsupported_response_type: response_type is not code.

  • invalid_scope: you ask for a scope that doesn't exist or that your app may not ask for. Separate scopes with a space; a comma gives this error.

  • invalid_request: the request is wrong, for instance a code_challenge_method other than S256, or an invalid prompt or max_age.

  • access_denied: the member clicked Deny.

  • login_required or consent_required: with prompt=none, when the member first has to log in or give permission.

If we don't know the client_id, the member sees a page that was not found. If an app on OpenID Connect names a redirect_uri that isn't registered for the app, we refuse the request.

Step 3: Exchange the code for a token

The code is valid for 60 seconds and works once. If a code is used a second time, we revoke the token from the first time.

POST to https://www.vereniging.nl/oauth/token with Content-Type: application/x-www-form-urlencoded and this body:

grant_type=authorization_code&code=<code>&code_verifier=<code_verifier>

  • If you named a redirect URI other than the first in step 1, send the same redirect_uri.

  • A confidential app authenticates with HTTP Basic: the Client ID as username, the client secret as password.

  • A public app doesn't use HTTP Basic, but sends client_id=<client_id> in the body.

Note! For a confidential app, your server makes this request, never the member's browser. Otherwise your client secret leaks.

Something went wrong

You get JSON back: {"error": "<error>"}. The error is one of:

  • invalid_client: the Client ID or client secret is wrong, or your app authenticates in a way that doesn't fit its client type.

  • invalid_grant: the code has expired, is wrong or was used before, the code_verifier doesn't match the code_challenge, your app is locked or the member is no longer active.

  • invalid_request: something is missing, such as the code, or a public app's code_verifier.

  • unsupported_grant_type: grant_type is not authorization_code or refresh_token.

  • unauthorized_client: your app may not use this grant_type, for instance refresh_token without Allow refresh tokens.

You get a token

{
  "access_token": "The access token",
  "token_type": "bearer",
  "expires_in": 240,
  "expires_at": "2026-11-11 13:46:13.820105",
  "scope": "openid profile email",
  "refresh_token": "The refresh token, if your app may use them",
  "user_id": 1234,
  "name": "The member's name",
  "id_token": "The ID token, with the openid scope"
}

  • access_token: with this you fetch the member's details in step 4.

  • expires_in and expires_at: when the access token expires, in seconds from now and as a time. Note! expires_at is in UTC, not Dutch time.

  • refresh_token: only when your app has Allow refresh tokens and is confidential. See step 5.

  • user_id: the member's ID in Congressus. This is not the username, but you can use it in the API.

  • id_token: a JWT saying which member logged in. Decode and verify it with a JWT library.

The ID token holds:

  • iss: who issued the token. On OpenID Connect the website's address, on Legacy urn:congressus:nl.

  • sub: the member's ID in Congressus.

  • aud: your app's Client ID.

  • name, gender and status_id: the member's name, gender and the ID of their current member status.

  • nonce: the nonce you sent in step 1.

  • exp, iat and auth_time: when the access token expires, when this token was issued and when the member logged in.

  • acr and amr: always urn:c2id:acr:silver and ["pwd"].

An app on OpenID Connect gets an ID token signed with RS256; the key is at https://www.vereniging.nl/.well-known/jwks.json. An app on Legacy gets an ID token signed with HS256, with the client secret as key.

Step 4: Fetch the member's details

GET https://www.vereniging.nl/oauth/userinfo with the header:

Authorization: Bearer <access_token>

You get the member's details as JSON, only for the scopes the member gave. Which fields each scope gives is explained in Which scopes are available? This needs the openid scope; without it you get 403 with insufficient_scope. An expired or revoked token gives 401 with invalid_token.

Step 5: Request a new access token with the refresh token

An access token expires after your app's Access token validity. If you still want to fetch the member's details after that, for instance to update them in the background, you request a new access token with the refresh token. The member doesn't have to do anything.

This works when your app has Allow refresh tokens switched on and is confidential. POST to https://www.vereniging.nl/oauth/token, with HTTP Basic as in step 3 and this body:

grant_type=refresh_token&refresh_token=<refresh_token>

You get a response as in step 3, with a new access token. The refresh token stays the same. It works until you revoke it (step 6), your app is locked or the member is no longer active.

Note! A refresh token gives lasting access to a member's details. Never share it with users and store it safely, just like your client secret.

Step 6: Revoke a token

When a member logs out of your application, or you no longer need a token, revoke it. POST to https://www.vereniging.nl/oauth/revoke, authenticating as in step 3, with this body:

token=<access_token or refresh_token>

The access token and the refresh token both stop working. You get {"success": "deleted_token"} back.

Related articles

Did this answer your question?