Developer guide

Add sign-in to your app with OpenID Connect.

Kweepa Identity is Kweepa's sign-in server, and your applications can use it too: people sign up and sign in with one account, by password or one-time code, with a second step where you ask for it, and your API reads their roles from the token.

How Kweepa Identity fits into your application

Kweepa Identity is a standard OAuth 2.0 and OpenID Connect server, and people have one account that works across applications. Your application sends them to sign in, receives a code, and exchanges it for tokens that your API can check.

At a glance

Issuer
https://auth.kweepa.com
Discovery
https://auth.kweepa.com/.well-known/openid-configuration
Flows
Authorization code with PKCE (S256 only), refresh tokens, and client credentials.
Tokens
JWTs signed with RS256. Keys at https://auth.kweepa.com/oauth2/jwks.
Client authentication
client_secret_basic, client_secret_post, or none for a public client.
Errors
OAuth 2.0 error bodies (RFC 6749).
  1. Your application sends the person

    To the authorization endpoint, with a PKCE challenge.

  2. They sign in on Kweepa Identity's pages

    By password or one-time code, with a second step where one is asked for. They are asked once whether your application may act in their name.

  3. Your application receives a code

    The browser comes back to your redirect URI with code and your state.

  4. It exchanges the code

    At the token endpoint, with the PKCE verifier, and its secret if it has one.

  5. It receives tokens

    An ID token, an access token and a refresh token.

  6. Your API checks the access token

    Its signature, its issuer, its audience, and the roles and permissions in it.

The words the guide uses

Account
One for each person. Its id is the sub of their tokens. The tokens say nothing else about the person: your application keeps whatever else it needs to know.
Client
Your application as Kweepa Identity knows it: a client id, its redirect URIs, how it proves who it is, and the APIs its tokens are for.
Application
The name of an API that reads Kweepa Identity's tokens, such as malipo. Roles and permissions belong to an application. A client is registered with the names of the applications its access tokens are for: its audience.
Roles and permissions
What a person may do in an application. They are written into the access token, so that your API checks a claim instead of a table of its own.

Register your application

Every application is a client. Kweepa registers it with you, so that its settings and the names of the APIs its tokens are for are right from the first sign-in. Tell us what it is called, where it runs and what its API is called.

A client is registered with the standard dynamic client registration endpoint (RFC 7591), which takes an initial access token that Kweepa holds, or in the administrators' console. This is what is decided:

Registration

POST /connect/register
Authorization: Bearer <initial access token>
Content-Type: application/json

{ "client_name": "Duka web",
  "redirect_uris": ["https://duka.example.com/login/oauth2/code/kweepa"],
  "post_logout_redirect_uris": ["https://duka.example.com/"],
  "grant_types": ["authorization_code", "refresh_token"],
  "scope": ["openid", "profile", "email"],
  "token_endpoint_auth_method": "client_secret_basic",
  "audience": ["duka-api"] }

The answer is 201, with client_id and client_secret and the metadata as registered. The secret is shown once and kept only as a hash. A public client, one with token_endpoint_auth_method none, never uses it.

What a client is registered with
Field What it is
client_name Shown to people when they are asked whether your application may act in their name. At most 200 characters.
redirect_uris Required. Where a browser may be sent with a code: HTTPS, or http://localhost for development. At most 20, with no fragment, no user name and no comma (write it as %2C).
post_logout_redirect_uris Where a browser may be sent after it signs out. The same rules.
grant_types authorization_code, refresh_token and client_credentials. The default is the first two.
scope openid, profile, email and offline_access. The default is the first three.
token_endpoint_auth_method client_secret_basic (the default), client_secret_post, or none for a public client, such as a browser or phone app, which signs in with PKCE and holds no secret.
access_token_ttl_seconds How long an access token lasts. The default is 1800 (30 minutes), from 1 second to 24 hours.
refresh_token_ttl_seconds How long a refresh token lasts. The default is 28800 (8 hours), from 1 second to 90 days.
require_proof_key true by default: the client has to use PKCE.
require_otp false by default. true asks for a one-time code at every sign-in to your application.
audience The names of the APIs your access tokens are for: up to 5, each a name or URI without spaces, at most 100 characters. Without it the tokens carry the client's own id as aud, which no API should accept. A name may not be a client's id or the issuer.

An example: a browser app

A browser app holds no secret, so it is a public client with PKCE, short-lived access tokens and one audience. Kweepa's own dashboards are registered like this: Malipo's dashboard has https://malipo.kweepa.com/dashboard/ as its redirect and post-logout address, openid profile email as its scope, exactly one audience, malipo, and access tokens of 15 minutes.

A public client

{ "client_name": "Malipo dashboard",
  "redirect_uris": ["https://malipo.kweepa.com/dashboard/"],
  "post_logout_redirect_uris": ["https://malipo.kweepa.com/dashboard/"],
  "grant_types": ["authorization_code"],
  "scope": ["openid", "profile", "email"],
  "token_endpoint_auth_method": "none",
  "access_token_ttl_seconds": 900,
  "audience": ["malipo"] }

People are asked first

Whoever signs in through a client is asked, on a page that names your application and the APIs it would act at, whether it may act in their name. What they say is kept, and they are not asked again until your application asks for more. Only Kweepa's own applications are marked as trusted, and skip the question.

Connect an OpenID Connect library

Any standard OpenID Connect client works. Everything it needs is in the discovery document.

curl

curl -s https://auth.kweepa.com/.well-known/openid-configuration

The endpoints
Endpoint Address
Authorization https://auth.kweepa.com/oauth2/authorize
Token https://auth.kweepa.com/oauth2/token
User info https://auth.kweepa.com/userinfo
Signing keys (JWKS) https://auth.kweepa.com/oauth2/jwks
Revocation https://auth.kweepa.com/oauth2/revoke
Introspection https://auth.kweepa.com/oauth2/introspect
End session https://auth.kweepa.com/connect/logout
Authorization challenge https://auth.kweepa.com/oauth2/authorize-challenge, for your own sign-in screens

In Spring Security a client needs its client id and secret and the issuer, from which the rest is read:

application.yml

spring:
  security:
    oauth2:
      client:
        registration:
          kweepa:
            client-id: ${KWEEPA_CLIENT_ID}
            client-secret: ${KWEEPA_CLIENT_SECRET}
            scope: openid,profile,email
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
        provider:
          kweepa:
            issuer-uri: https://auth.kweepa.com

PKCE is required

A client is registered with PKCE required unless it said it cannot, and the only method is S256. Check that your library sends a challenge. A phone app can use AppAuth, which reads the same discovery document.

Sign people in

  1. Make a PKCE pair

    The verifier is a random string of 43 to 128 characters (letters, digits and -._~). The challenge is its SHA-256 hash, base64url encoded with no padding. Keep the verifier for step 3.

    Shell

    CODE_VERIFIER=$(openssl rand -hex 32)
    CODE_CHALLENGE=$(printf %s "$CODE_VERIFIER" | openssl dgst -sha256 -binary \
      | openssl base64 -A | tr '+/' '-_' | tr -d '=')
    

  2. Send the browser to the authorization endpoint

    Request

    GET https://auth.kweepa.com/oauth2/authorize
      ?response_type=code
      &client_id=CLIENT_ID
      &redirect_uri=https%3A%2F%2Fduka.example.com%2Fcallback
      &scope=openid%20profile
      &state=RANDOM
      &nonce=RANDOM
      &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
      &code_challenge_method=S256
    

    Parameters
    Parameter What it is
    response_type code.
    client_id Your client's id.
    redirect_uri One of those the client was registered with, exactly.
    scope From those the client was registered with. Without openid there is no ID token.
    state A random value of yours. It comes back unchanged: check it.
    nonce Optional. Copied into the ID token.
    code_challenge, code_challenge_method PKCE. The method is S256.

    The person signs in on Kweepa Identity's pages and is asked about your application if they have not been. The browser then comes back to your redirect_uri with code and your state.

  3. Exchange the code for tokens

    From your server, or from the app if it is a public client.

    Web app, with a secret

    curl -s https://auth.kweepa.com/oauth2/token \
      -u "$CLIENT_ID:$CLIENT_SECRET" \
      -d grant_type=authorization_code \
      -d code="$AUTHORIZATION_CODE" \
      -d redirect_uri=https://duka.example.com/callback \
      -d code_verifier="$CODE_VERIFIER"
    

    Public client

    curl -s https://auth.kweepa.com/oauth2/token \
      -d client_id="$CLIENT_ID" \
      -d grant_type=authorization_code \
      -d code="$AUTHORIZATION_CODE" \
      -d redirect_uri=https://duka.example.com/callback \
      -d code_verifier="$CODE_VERIFIER"
    

    The code lasts a few minutes and works once. The answer is the usual token response: access_token, refresh_token, id_token (with openid) and expires_in.

Check tokens in your API

An API can tell a token meant for it from any other token of this issuer by finding its own name in aud. That is why a client is registered with an audience.

An access token
Claim What it says
iss https://auth.kweepa.com.
sub The account that signed in.
aud The names of the APIs the token is for, as the client was registered. Yours must be among them.
client_id The client the token was issued to.
exp When it ends.
roles The names of the person's roles in the API. Left out when there are none.
permissions The codes of their permissions in the API. Left out when there are none.
  1. Read the signing keys

    Fetch https://auth.kweepa.com/oauth2/jwks and keep the keys by kid. Tokens are signed with RS256. Signing keys are rotated on a schedule, so fetch again when a token names a key you do not have.

  2. Check the token

    The signature, iss, exp, and that aud holds your API's name. A token that fails any of these is not for you.

  3. Read what the person may do

    Check roles or permissions, instead of a table of your own.

What the roles and permissions in a token mean

They are the ones the account holds in that API at the moment the token is issued, worked out anew for every token, a refreshed one included. A role given or taken back since is in the next token, and not in the one you hold. A token for several APIs carries neither claim, nor does an ID token, nor a token of a client acting for itself.

When you cannot check a token yourself

  • POST /oauth2/introspect answers whether a token is active, for a client that authenticates.
  • GET /userinfo with the access token answers sub. The ID token is for your client alone: its aud is the client id, and it says who signed in and nothing about what they may do.

Keep people signed in, and sign them out

  1. Refresh before the access token ends

    curl

    curl -s https://auth.kweepa.com/oauth2/token \
      -u "$CLIENT_ID:$CLIENT_SECRET" \
      -d grant_type=refresh_token \
      -d refresh_token="$REFRESH_TOKEN"
    

    Refresh tokens rotate

    Every refresh answers with a new refresh token, and the one you sent is used up. Keep the new one each time. A refresh token used twice ends the sign-in, so that a copied one shows itself the moment both are used.

  2. Revoke on sign-out

    curl

    curl -s https://auth.kweepa.com/oauth2/revoke \
      -u "$CLIENT_ID:$CLIENT_SECRET" \
      -d token="$REFRESH_TOKEN"
    

    Then forget the tokens. To end the person's session on Kweepa Identity's own pages as well, send the browser to the end session endpoint with the id_token_hint and one of the client's registered post-logout addresses.

  • An access token lasts 30 minutes and a refresh token 8 hours unless the client was registered with other lifetimes, up to 24 hours and 90 days.
  • No token of any kind is issued for an account that is no longer active. A refresh for an account suspended or deleted since it signed in is refused with invalid_grant, whatever is left of its lifetime.

Give people roles and permissions in your API

Roles and permissions belong to an application: your API's name, the one you registered as the client's audience. They are kept in one place and written into your access tokens.

Permission
What can be done in your API, by a code of your own: orders:refund. A catalogue of them for each application.
Role
A name for a set of permissions: SUPPORT. A role can be marked as a default, which every new account is given.
Grant
A role or a permission given to an account, with a date it ends on if you want one. What changes is in the account's next token.
Names
Letters, digits and : . _ -, starting with a letter or a digit, at most 100 characters. A name goes into tokens as it is.

Manage them from your own back office

The management API does what the administrators' console does. Kweepa gives your team the access to manage your application's roles, or manages them with you.

The management API
Call What it does Needs
GET /management/applications The applications that have a role or a permission. permissions:read
GET /management/permissions?application= The catalogue, for one application or all. permissions:read
POST /management/permissions Add a permission: {"application","code","description"}. permissions:manage
PUT, DELETE /management/permissions/{permissionId} Describe a permission anew, or remove it, and with it take it from every role and account that had it. permissions:manage
GET /management/roles?application= The roles, for one application or all. roles:read
POST /management/roles {"application","name","description","defaultRole","permissions":[codes]} roles:manage
PUT, DELETE /management/roles/{roleId} Replace a role's definition (every field is sent), or remove it from every account that held it. roles:manage
GET /management/accounts?identity= Find the account somebody signs in to, by the username, email address or phone number they use, whole. accounts:read
GET /management/accounts/{accountId}/grants The roles an account holds and the permissions given to it directly. grants:read
POST /management/accounts/{accountId}/roles {"roleId","expiresAt"}. 201, or 409 when the account holds it. grants:manage
POST /management/accounts/{accountId}/permissions {"permissionId","expiresAt"}. 201, or 409 when it was given already. grants:manage
DELETE /management/accounts/{accountId}/roles/{roleId}, .../permissions/{permissionId} Take a grant back. Taking back what is not held changes nothing. grants:manage

It is an API called with a bearer token, never with a session. The token is an ordinary access token for the audience authorization-server alone, issued to a client that an administrator registered for it, for an account that holds the permissions above. A token that was revoked or replaced when it was refreshed is refused although its dates are still good. Where administrators are required to have a second factor, the sign-in has to have taken one.

Roles of the server itself are not yours

Whoever may give out roles of another application must not be able to make an administrator by it, so anything that touches the roles, permissions and grants of Kweepa Identity itself asks for one more permission, which only administrators hold.

Two-step sign-in, and how people sign in

People sign in and sign up on Kweepa Identity's own pages, through the ordinary code flow.

  • With an email address, a phone number or a username and a password.
  • With a code sent by SMS or email instead of a password. Sign-up is confirmed by a code.
  • With Google, Facebook, Telegram, LinkedIn, GitHub or X.
  • With a second step, for anybody who has turned on an authenticator app (with recovery codes).

To make the second step compulsory for your application, register its client with require_otp. Every sign-in to it then ends with a code from an authenticator app, a text or an email, or a recovery code. An account that signed up with a username is asked for the code that was sent to the email address it verified.

Your own sign-in screens

An application of your own can show its own sign-in screens instead of sending people to auth.kweepa.com. Its server posts what the person typed to the sign-in API and gets an authorization code back, which it exchanges at the token endpoint like any other. This follows the IETF draft OAuth 2.0 for First-Party Applications, version 04.

  1. The person types

    On your screen, in a browser or a phone app.

  2. Your server posts it

    To POST /oauth2/authorize-challenge, with the client secret.

  3. Kweepa Identity answers

    With an authorization code, or with the next step to ask for.

  4. Your server exchanges the code

    At /oauth2/token, with the PKCE verifier.

  5. Your server keeps the tokens

    In its own session. The browser gets a cookie, never a token.

Before you start

Only your own applications. An application with its own screens sees people's passwords, so anybody else's application signs people in through Kweepa Identity's pages. It needs a server that keeps the client secret, a backend for your front end: the secret never goes into a browser or a phone app, and this API never answers a request without it. And an administrator turns it on for your client.

  • POST https://auth.kweepa.com/oauth2/authorize-challenge, with a body of application/x-www-form-urlencoded. Nothing in the URL: a request with a query string is refused.
  • The client authenticates with its secret as at the token endpoint: HTTP Basic, or client_id and client_secret in the body if it was registered that way.
  • X-End-User-IP: <the person's IP address> on every request: see below.
  • Answers are JSON with Cache-Control: no-store. Every parameter at most once.
  1. Password

    curl

    curl -s https://auth.kweepa.com/oauth2/authorize-challenge \
      -u "$CLIENT_ID:$CLIENT_SECRET" \
      -H "X-End-User-IP: 203.0.113.7" \
      -d response_type=code \
      --data-urlencode "scope=openid profile" \
      -d code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM \
      -d code_challenge_method=S256 \
      --data-urlencode "username=alice@example.com" \
      --data-urlencode "password=$PASSWORD"
    

    Parameters
    Parameter What it is
    response_type code, required on the first request.
    scope From those the client was registered with. Without openid there is no ID token.
    code_challenge, code_challenge_method PKCE, S256 only. Required when the client was registered with PKCE, which is the default.
    nonce Optional. Copied into the ID token.
    username A username, an email address or a phone number.
    password The password.
    known_device Optional: what the last sign-in of this person on this device gave you. See below.

    The answer is 200 with {"authorization_code": "…", "known_device": "…"}. Keep known_device on the person's device, and go to step 4. Otherwise it is one of the answers in the table below.

  2. One-time code by email or SMS

    curl

    curl -s https://auth.kweepa.com/oauth2/authorize-challenge \
      -u "$CLIENT_ID:$CLIENT_SECRET" -H "X-End-User-IP: 203.0.113.7" \
      -d response_type=code --data-urlencode "scope=openid profile" \
      -d code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM -d code_challenge_method=S256 \
      --data-urlencode "username=alice@example.com" \
      -d send_otp=true
    

    username is an email address or a phone number the account has verified. The answer is always 403 with otp_required and an auth_session, whether or not the address belongs to an account, so that nobody can find out who has one. Then send the code the person typed:

    curl

    curl -s https://auth.kweepa.com/oauth2/authorize-challenge \
      -u "$CLIENT_ID:$CLIENT_SECRET" -H "X-End-User-IP: 203.0.113.7" \
      -d auth_session="$AUTHENTICATION_SESSION" -d otp=482913
    

    The answer is 200 with the code, 403 with mfa_required when the account also has an authenticator app, or 400 invalid_grant with the auth_session again for a wrong code: let the person try again. A code is good only in the auth_session it was sent for.

  3. Second factor

    curl

    curl -s https://auth.kweepa.com/oauth2/authorize-challenge \
      -u "$CLIENT_ID:$CLIENT_SECRET" -H "X-End-User-IP: 203.0.113.7" \
      -d auth_session="$AUTHENTICATION_SESSION" --data-urlencode "otp=123 456"
    

    otp is the six digits of the authenticator app, spaces allowed, or one of the person's recovery codes. After too many wrong codes, or when the step has expired, the answer is 400 invalid_session: start again from step 1.

  4. Exchange the code

    curl

    curl -s https://auth.kweepa.com/oauth2/token \
      -u "$CLIENT_ID:$CLIENT_SECRET" \
      -d grant_type=authorization_code \
      -d code="$AUTHORIZATION_CODE" \
      -d code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
    

    No redirect_uri: none was used. The tokens are the same as after a browser sign-in. Refresh as before. Sign out by revoking the refresh token at POST /oauth2/revoke and forgetting the tokens: there is no session cookie on this server to end.

The person's address and device

X-End-User-IP
Your server sends the sign-in steps of all its users from one address. Send each person's own address, one IPv4 or IPv6 address, so that the limits count each person and not your server. Take it from your own proxy as you trust it, for example nginx's $remote_addr, never from a header the browser wrote. Without it every request counts as your server's address.
known_device
Every 200 answer carries one. Keep it on the person's device, in a cookie of your own site that lasts a few months (HttpOnly, Secure, SameSite) or in a phone app's secure storage, and send it back with that person's next sign-in from that device. It signs nobody in and skips no step: it only decides whose limits the codes of a sign-in count against, so that a stranger who knows somebody's address cannot use up the owner's codes. It is good for 180 days after the last sign-in.

The answers

What the endpoint answers
Status Error What to do
200 authorization_code: exchange it. known_device: keep it on the person's device.
403 insufficient_authorization with mfa_required Ask for the authenticator code, and send it with auth_session.
403 insufficient_authorization with otp_required Ask for the code that was sent, and send it with auth_session.
403 insufficient_authorization with otp_sign_in_required The application asks for a one-time code at every sign-in: sign in with a code instead.
403 redirect_to_web The person has to finish in a browser: start an ordinary authorization code flow with PKCE.
400 invalid_grant Wrong password or code. With auth_session, the person may try that step again. An unknown username, an account that cannot sign in and an account locked after too many wrong passwords get the same answer, on purpose.
400 invalid_session The sign-in ended or expired: start again.
400 invalid_request, invalid_scope, unsupported_response_type A mistake in the request. error_description says which.
400 unauthorized_client The client is not allowed its own sign-in screens, or has no authorization code grant.
401 invalid_client Wrong or missing client credentials.
429 temporarily_unavailable Too many attempts: wait for Retry-After seconds.

Signing up, resetting a password, signing in with Google, Facebook, Telegram, LinkedIn, GitHub or X, and setting up an authenticator app stay on Kweepa Identity's pages. Open the ordinary authorization code flow for them, as you do when the API answers redirect_to_web.

Your server's side

  • HTTPS only, and never log the request bodies you send here or receive from your users.
  • Keep the client secret in your server's vault or environment, never in front-end code.
  • Do not keep passwords or codes after the request. Keep the auth_session only for the person's sign-in in progress, and the refresh token encrypted.
  • Treat known_device like a session cookie.
  • Show Kweepa Identity's error_description or your own words, but do not tell people whether an account exists.

Errors and limits

The token endpoint
Answers with the standard OAuth 2.0 errors: invalid_request, invalid_client (401), invalid_grant, unauthorized_client, unsupported_grant_type and invalid_scope. An authorization code works once and lasts a few minutes.
Registration
401 invalid_token without the initial access token, 400 invalid_client_metadata for a value that is not allowed, with error_description saying which, and 429 temporarily_unavailable for a caller that keeps presenting a wrong token.
The management API
401 without a token Kweepa Identity accepts for it, 403 without the permission the call needs, 404 for what is not there, 409 for a grant that is held already.
Sign-in limits
Each person, by the address you send, has a number of sign-in steps a minute. Each account is locked against passwords after too many wrong ones, though a one-time code still signs it in. Codes and second factors have budgets of their own for each email address or phone number. Beyond a limit the answer is 429 with Retry-After.

Common questions

Which libraries work with it?

Any standard OpenID Connect client, Spring Security and AppAuth among them: everything a library needs is in the discovery document at auth.kweepa.com.

Can a web or mobile app use it without a secret?

Yes. A public client signs in with the code flow and PKCE and holds no secret. A server-side application is given a secret, shown once and kept only as a hash.

How do we make two-step sign-in compulsory?

Your application's client is set to require a one-time code. Every sign-in to it then ends with a code from an authenticator app, a text or an email, or a recovery code.

What do the tokens carry?

The ID token says which account signed in. An access token for your API carries its audience, your client, and the person's roles and permissions in your application, worked out again at every refresh.

Ready to add sign-in?

Tell us what your application is, where it runs and what its API is called. We will register it and say which flow fits.

Ask for a client