# Add a custom OpenID Connect provider

Add a sign-in button for an identity provider BuildBase has not built, such as Keycloak, Okta or your own, with every field and URL check explained.

A custom provider puts one more sign-in button on your hosted page, for an identity provider BuildBase has not built in: a Keycloak server, Okta, Auth0, or a sign-in service your company runs itself. It works with any provider that speaks OpenID Connect (OIDC, the standard way one site confirms who someone is for another), or plain OAuth 2.0 with a user info address. The person presses the button, signs in on your provider's page, and comes back to your app signed in. You make a client (an application) in the provider, and swap a few values with BuildBase. The screenshots below use a small test provider named **Acme ID**, run on its own server, and show a real sign-in through it.

> **Note:**

- An identity provider where you can register a client: its **issuer URL**, a **client ID** and a **client secret**.
- **One per organization.** Every app in it can tick the same custom provider. Once one exists, **Custom OIDC provider** is no longer offered in **Add new method**, and adding a second is refused with "This app already has a custom provider. Edit that one instead."
- A plan that includes the custom OIDC sign-in provider. A plan without it shows the choice locked with "Your plan does not include custom OIDC sign-in provider.", and adding one is refused. While your plan does not name the feature at all, the add goes through. It also counts toward your plan's limit of social sign-in providers.



## What goes where

| Copy this     | From                                                         | Paste it into                                     |
| ------------- | ------------------------------------------------------------ | ------------------------------------------------- |
| Issuer URL    | Your provider, such as `https://id.acme.example/realms/acme` | BuildBase, **Issuer URL**                         |
| Client ID     | Your provider, the client you made                           | BuildBase, **Client ID**                          |
| Client secret | Your provider, the same client                               | BuildBase, **Client secret**                      |
| Redirect URL  | BuildBase, after you press **Create**                        | Your provider, the client's allowed redirect URLs |

Okta calls the last one **Sign-in redirect URIs**, Auth0 **Allowed Callback URLs**, and Keycloak **Valid redirect URIs**.

## 1. Make a client in your provider

These are the steps the console shows under **How to connect your identity provider**. In Keycloak, as an example:

1. In your realm, open **Clients** and choose **Create client**. Keep **OpenID Connect** as the type, and give a **Client ID**, such as `acme-analytics`.
2. Turn **Client authentication** on, so the client has a secret, and keep **Standard flow**, the one that sends the person to the sign-in page and back.
3. Leave **Valid redirect URIs** for now: you paste BuildBase's at the end of the next part.
4. On the client's **Credentials** tab, copy the **Client secret**.
5. Your issuer URL is your Keycloak address with `/realms/` and the realm's name, such as `https://id.acme.example/realms/acme`. In Okta it is your Okta address, and in Auth0 your Auth0 domain with `https://` in front.

## 2. Add the provider in the BuildBase console

1. In the console, open **User management**, then **Authentication** under **Access**, and the **Methods** tab.
2. Press **Add auth method** and choose **Custom OIDC provider**.

![The Add new method dialog with Custom OIDC provider chosen: what it is for, that it allows one per app, and the open guide How to connect your identity provider with its five steps](/images/sign-in-setup/custom-oidc/console-choose.webp)

3. Fill in the form. Each field:

| Field                            | What to give it                                                                                                                                    |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Title**                        | Your own name for the method, shown in the console only. Here `Acme ID`.                                                                           |
| **Button label**                 | Required. What the button on the sign-in page says, such as `Acme ID`.                                                                             |
| **Issuer URL**                   | For a provider with OIDC discovery. Paste it without `/.well-known/openid-configuration`: BuildBase reads every other address from that document.  |
| **Client ID**, **Client secret** | Required. From the client you made.                                                                                                                |
| **Authorization URL**            | Only for a provider without discovery: the page where people type their password. Give all three addresses and leave **Issuer URL** empty.         |
| **Token URL**                    | Only without discovery: where BuildBase collects the sign-in, behind the scenes.                                                                   |
| **User info URL**                | Only without discovery: where BuildBase reads the person's email address and name.                                                                 |
| **Icon URL**                     | A square image over `https://`. Without one, or if it fails to load, the button shows a key.                                                       |
| **Trust its email addresses**    | **Never** (the default), **When it sends email_verified: true**, or **Always**. See [Trusting its email addresses](#trusting-its-email-addresses). |
| **Scopes**                       | `openid profile email` by default. Keep them: a new account is made with the email address.                                                        |

![The custom provider form: Title Acme ID, Button label Acme ID, Issuer URL https://id.acme.example/realms/acme, Client ID acme-analytics, Client secret as dots, the three endpoint boxes and Icon URL left empty, Trust its email addresses on Never, and the scopes openid profile email](/images/sign-in-setup/custom-oidc/console-form.webp)

4. Press **Create**. BuildBase reads the provider's discovery document before it saves, so a mistyped issuer is refused now rather than at someone's sign-in:

![A message in the console: Could not add Custom OIDC provider. https://id.acme.example/realms/acme/.well-known/openid-configuration does not resolve to a public address BuildBase can reach.](/images/sign-in-setup/custom-oidc/console-issuer-refused.webp)

5. With a working issuer, the dialog says **One step left** and shows the **Redirect URL**. Press **Copy**, then **Done**.

![After Create: One step left, and the Redirect URL box with the address to add to the allowed redirect URLs of the application in your identity provider](/images/sign-in-setup/custom-oidc/console-redirect-url.webp)

6. In your provider, paste the URL into the client's allowed redirect URLs (Keycloak: **Valid redirect URIs**) and save.

The redirect URL has this shape:

```text
https://api.console.buildbase.app/api/v1/auth/social/custom/<orgId>/<methodId>/callback
```

It starts with your BuildBase server, https://api.console.buildbase.app on BuildBase's cloud.

### The URL checks

- **Discovery or three addresses.** Saving with neither an issuer nor all three addresses is refused with "Give an issuer URL, or all three endpoints."
- **Every address BuildBase calls must be public.** The issuer, and the token, JWKS (signing keys) and user info addresses, including the ones the discovery document names, must resolve to a public address. One that does not is refused with "... does not resolve to a public address BuildBase can reach." A provider on a private network has to be reachable at a public address first. A self-hosted BuildBase may use addresses on its own network; cloud metadata and link-local addresses are refused everywhere.
- **The authorization URL is not checked.** BuildBase never calls it; it only sends the person's browser there.
- **The discovery document must name the same issuer** it was fetched for, and list authorization, token and JWKS addresses. Otherwise the save is refused with "The discovery document names a different issuer" or "The discovery document has no authorization, token or JWKS endpoint."
- **`https://` only** on a BuildBase server running in production: a plain `http://` address is refused with, for example, "Issuer URL must be an https:// URL." **Icon URL** is always `https://`, everywhere: "Icon URL must be an https:// URL."

## 3. Attach it to your app

1. Open the **Settings** tab and press **Edit** on **Acme Analytics**.
2. Under **Methods**, tick the provider, listed by its **Title**, here **Acme ID**, and press **Update**.

![The Edit auth setting sheet for Acme Analytics, at Methods: Acme ID ticked beside Email and password](/images/sign-in-setup/custom-oidc/console-attach.webp)

## 4. Check the preview

The **Preview** on the **Sign-in page** tab shows your look with sample GitHub and Google buttons, not your methods. Press **Open the live page for Acme Analytics** above it to see the real page with your button.

## 5. What people see

The hosted sign-in and register pages now have a button with your **Button label**. With no **Icon URL**, it shows a key.

![The hosted sign-in page Sign in to Acme with an Acme ID button, showing a key icon because no Icon URL is set](/images/sign-in-setup/custom-oidc/hosted-button.webp)

On a phone it runs the full width under the form:

![The sign-in page on a phone 390 pixels wide, with the Acme ID button across the full width under the form](/images/sign-in-setup/custom-oidc/hosted-phone.webp)

Here is a whole first sign-in, for Riley Chen, who has no account yet:

1. Riley presses **Acme ID**. Their browser goes to the provider's own page, which asks them to sign in, and the first time to share their name and email address with your app.

![The identity provider page, here a test provider named Acme ID: signed in as Riley Chen, riley.chen@acme.example, asking to share name and email with Acme Analytics, with Allow and Cancel](/images/sign-in-setup/custom-oidc/provider-page.webp)

2. The provider sends them back, and BuildBase makes their account with the name and address the provider sent. Trust is still **Never**, so BuildBase has not taken the address on the provider's word: the sign-in page asks Riley to type a 6-digit code sent to it. With **When it sends email_verified: true** and a provider that says so, this step is skipped.

![Back on the hosted page: Verify your email, with a box for the 6-digit code sent to riley.chen@acme.example](/images/sign-in-setup/custom-oidc/hosted-verify-email.webp)

3. After the usual **Trust this device?** question (and the passkey offer, if your app offers passkeys), Riley is in your app.

![The app page Acme Analytics after the round trip: Signed in as Riley Chen, riley.chen@acme.example](/images/sign-in-setup/custom-oidc/app-signed-in.webp)

Riley now appears under **Users** in the console, with Sam Rivera from the next section:

![The Users list in the console with Riley Chen, who signed up through Acme ID, and Sam Rivera, who already had an account](/images/sign-in-setup/custom-oidc/console-users.webp)

## Trusting its email addresses

Trust decides whether this provider may sign someone into an account that already has the same email address. BuildBase knows nothing about a provider you added, so it starts at **Never**.

| Setting                                | What it means                                                                                                                                                                                                  |
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Never** (default)                    | It cannot sign into an account that already exists with the same address. It can create new accounts, which confirm their address with an emailed code, and it finds them again by the provider's own user ID. |
| **When it sends email_verified: true** | It can sign into an existing account when, on that sign-in, it says the address is verified. Keycloak, Okta and Auth0 send `email_verified`.                                                                   |
| **Always**                             | Every address it sends is treated as verified.                                                                                                                                                                 |

Trust a provider only if it checks that people own the addresses it sends. Trusting one where anyone can type in any address lets anyone sign into anyone's account.

With **Never**, Sam Rivera, who already has an account at `sam.rivera@acme.example`, is turned away when he first tries the button:

![The sign-in page back from Acme ID with the message This email is already registered. Sign in the way you did before, then connect this provider from your account's security settings.](/images/sign-in-setup/custom-oidc/hosted-email-unverified.webp)

Sam can sign in as before and connect Acme ID from the Security screen of your app. Or, for a provider that checks addresses, change the trust: press the edit button on the provider's row in **Methods**, choose **When it sends email_verified: true**, and press **Save**.

![Editing Acme ID: Trust its email addresses, with When it sends email_verified: true chosen instead of Never or Always](/images/sign-in-setup/custom-oidc/console-trust.webp)

Sam's next press of the button signs him into his own account and connects Acme ID to it, so later sign-ins find him by the provider's user ID:

![The app page after Sam Rivera signs in with Acme ID once trust allows it: Signed in as Sam Rivera, sam.rivera@acme.example, his existing account](/images/sign-in-setup/custom-oidc/app-signed-in-linked.webp)

## What BuildBase reads from your provider

With discovery, BuildBase checks the ID token: its signature against the provider's JWKS (RS256, ES256 or PS256), the issuer, the audience (your client ID) and the sign-in's nonce. The request carries a PKCE challenge, and the client signs in at the token address with HTTP Basic, unless the provider supports only `client_secret_post`. Without discovery, the profile comes from the user info address, and the client secret goes in the form.

| Field                                                                  | Used for                                                                            |
| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `sub`, or `id` from a plain OAuth 2.0 user info                        | The provider's user ID, which finds the account on every later sign-in              |
| `email`                                                                | The account's address. Without one, no account can be made                          |
| `email_verified`                                                       | Whether the address is proven, when trust is **When it sends email_verified: true** |
| `name`, else `given_name` and `family_name`, else `preferred_username` | The name on a new account                                                           |

**Edit, do not replace.** People who signed in with the provider are linked to it. A new custom provider starts with no links, so after deleting and adding one again, those people are found only by email, which needs the trust above. Change the issuer, keys or label with the edit button instead.

## In your app

Nothing changes in your code. People connect and remove the provider on the SDK's Security screen, listed under its button label: see [Account security screens](/sign-in-setup/account-security-screens).

```tsx
import { useSaaSAuth } from '@buildbase/sdk/react';

export function SecurityButton() {
  const { openWorkspaceSettings } = useSaaSAuth();
  return (
    <button onClick={() => openWorkspaceSettings('security')}>Security</button>
  );
}
```

## Check it works

- [ ] Saving the provider succeeded, so BuildBase could read its discovery document.
- [ ] The redirect URL is in the client's allowed redirect URLs at the provider.
- [ ] The provider is ticked on your app, and **Open the live page** shows your button.
- [ ] A new person can sign in and lands in your app; they appear under **Users**.
- [ ] **Trust its email addresses** is what you decided, not left at **Never** by accident if your provider verifies addresses.

## Things to watch out for

- **ID tokens must be signed with RS256, ES256 or PS256.** A client set to sign with a shared secret (HS256) fails every sign-in with "Sign-in with that provider failed." In Auth0, keep the application's JSON Web Token signature algorithm on RS256.
- **The issuer must match the provider's own.** Copy the `issuer` value from the discovery document. In Keycloak it ends with `/realms/` and the realm's name. A slash at the end is ignored, anything else is not.
- **Trusting addresses can hand over accounts.** **Always** and **When it sends email_verified: true** let the provider sign into any existing account with the same address. Choose them only for a provider that checks people own the addresses it sends.
- **Deleting the provider unlinks everyone.** People are linked to this one provider. A new one starts with no links, so edit the existing provider to change its issuer, keys or label.
- **Changes at your provider can take an hour to reach BuildBase.** BuildBase keeps the discovery document it read for up to an hour.
- **Finish at your provider within 10 minutes.** A sign-in left open on your provider's page for more than 10 minutes cannot finish: BuildBase sends the browser to your sign-in page, with a note that the sign-in expired. Start again from your app's **Sign in** button.

## If something goes wrong

| What you see                                                                                                                                            | Why                                                                                                                                 | What to do                                                                                |
| ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| In the console: Give an issuer URL, or all three endpoints.                                                                                             | Neither an issuer nor the three addresses                                                                                           | Give the issuer, or the authorization, token and user info URLs.                          |
| In the console: ... does not resolve to a public address BuildBase can reach.                                                                           | The address is mistyped, or the provider is on a private network (`url_not_public`)                                                 | Check the address; make the provider reachable at a public address.                       |
| In the console: BuildBase could not read ... /.well-known/openid-configuration. Check the issuer URL.                                                   | The discovery document is not at that address (`discovery_failed`)                                                                  | Paste the issuer exactly as the provider names it, without the `/.well-known` part.       |
| In the console: The discovery document names a different issuer (...).                                                                                  | The issuer you typed differs from the one the provider states, often by a trailing part                                             | Copy the `issuer` value from the discovery document.                                      |
| In the console: This app already has a custom provider. Edit that one instead.                                                                          | There is one already (`custom_provider_exists`)                                                                                     | Edit the existing one.                                                                    |
| In the console: Your plan does not include custom OIDC sign-in provider.                                                                                | Your plan withholds the feature                                                                                                     | Upgrade.                                                                                  |
| The provider's page says the redirect URI is not valid                                                                                                  | The redirect URL is not in the client's allowed list                                                                                | Paste it from the console.                                                                |
| Sign-in with that provider failed. Please try again.                                                                                                    | BuildBase could not finish: a wrong client secret, an ID token it could not verify, or the provider unreachable (`provider_failed`) | Check the secret and the client's settings at the provider.                               |
| Sign-in with that provider did not finish. Please try again.                                                                                            | The person cancelled at the provider, or it answered with an error (`provider_error`)                                               | Try again.                                                                                |
| This email is already registered. Sign in the way you did before, then connect this provider from your account's security settings.                     | An account has this address and trust does not allow the provider to sign into it (`email_unverified`)                              | The person connects the provider from the Security screen, or you change the trust.       |
| This provider did not share an email address, so an account cannot be created. Allow access to your email address and try again.                        | The provider sent no `email` (`email_missing`)                                                                                      | Keep the `email` scope, and check the provider releases the address to this client.       |
| Sign-up is invite-only here, and there is no account for that address yet. Ask the team for an invitation, or sign in with an account you already have. | Public sign-up is off (`signup_disabled`)                                                                                           | Invite the person, or open sign-up. See [who can sign up](/sign-in-setup/sign-up-access). |
| This app is not taking new sign-ups right now.                                                                                                          | Your plan's end-user limit is reached (`signup_unavailable`)                                                                        | Upgrade, or remove users you no longer need.                                              |

## Next steps

- [Social providers](/authentication/social-providers) - the reference for the custom provider and every built-in one.
- [Who can sign up](/sign-in-setup/sign-up-access) - who a provider may create accounts for.
- [Account security screens](/sign-in-setup/account-security-screens) - connected accounts inside your app.
- [Set up sign-in, step by step](/sign-in-setup/overview) - every sign-in guide, and the order to set them up in.
