# Add sign-in to your app with OpenID Connect.

*Web page: https://kweepa.com/developers/kweepa-identity · Updated 2026-11-01*

> Sign-in, sign-up and single sign-on for your applications, through OAuth 2.0 and 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.

**Who it is for:** Developers of web, mobile and server applications who want sign-in, two-step verification and roles without building them, and organisations whose applications should share one account per person.

## What it does

- **Standard OAuth 2.0 and OpenID Connect**: The code flow with PKCE, refresh tokens that rotate, ID tokens, a discovery document and published signing keys, so that any OpenID Connect library can use it.
- **Passwords or one-time codes**: People sign in with an email address, a phone number or a username and a password, or with a code sent by SMS or email instead, and sign up with their details confirmed by a code.
- **Two-step sign-in**: Authenticator apps, with recovery codes, for anyone who turns them on, and a one-time code asked for at every sign-in to an application whose settings require one.
- **Roles and permissions in the token**: Each application's roles and permissions kept in one place and written into its access tokens, so that your API checks a claim instead of a table of its own.
- **Administration**: Applications, roles and accounts managed in a console and through an API: an account suspended, its two-step sign-in reset, or a role granted until a date.
- **Security built in**: Passwords hashed with Argon2, accounts locked after repeated wrong passwords, every sign-in step rate-limited, signing keys rotated on a schedule, and an audit log that cannot be altered.

## What you get

- Sign-in and sign-up without building them
- One account per person across your applications
- Roles your API can trust, read from the token

## Connects to

- OAuth 2.0
- OpenID Connect
- PKCE
- JWT access tokens
- Authenticator apps (TOTP)
- SMS and email codes
- Spring Security and AppAuth

## 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](https://datatracker.ietf.org/doc/html/rfc6749#section-5.2)).

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](https://datatracker.ietf.org/doc/html/rfc7591)), which takes an initial access token that Kweepa holds, or in the administrators' console. This is what is decided:

```http
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.

```json
{ "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.

```bash
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](#own-screens) |

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

```yaml
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

### Step 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.

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

### Step 2: Send the browser to the authorization endpoint

```http
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`.

### Step 3: Exchange the code for tokens

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

**Web app, with a secret**

```bash
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**

```bash
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. |

### Step 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.

### Step 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.

### Step 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

### Step 1: Refresh before the access token ends

```bash
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.

### Step 2: Revoke on sign-out

```bash
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](https://datatracker.ietf.org/doc/draft-ietf-oauth-first-party-apps/), 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.

### Step 1: Password

```bash
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.

### Step 2: One-time code by email or SMS

```bash
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:

```bash
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.

### Step 3: Second factor

```bash
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.

### Step 4: Exchange the code

```bash
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`.

## Questions people ask

### 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.

## Next step

Request a demo: https://kweepa.com/contact?service=kweepa-identity · Email: ops@kweepa.com
Provider: Kweepa Technologies, Nakuru, Kenya (https://kweepa.com/)
