A guide to OAuth2 and OpenID Connect
Setting-up an OAuth2 app for the first time might seem daunting, but once you understand the flow you'll be easily able to implement single sign-on and OAuth2-based API access for your apps in no time.
TL;DR?
If you're already familiar with OAuth2 and OpenID Connect, then you can simply check out the available URLs and scopes directly from your SSO Tools IdP dashboard to get started.
OAuth2 Overview
OAuth2 allows services to work together to enable single sign-on, or two allow the two services to communicate with each other on a user's behalf. When you choose options like "login with Facebook" or "connect to Stripe", OAuth2 is what is powering the interactions behind the scenes.
Under OAuth2 the user logs-in directly with their identity provider (IdP), and authorizes the IdP to reveal information about them declared in the "scope" (such as a user ID, email address, or even profile information). This information can then be used by the calling service provider ("app") in order to provision the user with an account or to log the user in. The app never sees the username and password used to login and only has access to the information declared in the scope authorized by the user.
Some IDPs may also issue an "access token" during the authentication process. This token can be used for ongoing communication between the two services. For example, the app might use the access token to retrieve or update the user's profile on the IdP, or carry out other actions (such as handling a payment with Stripe).
What is the OpenID Connect part about?
OpenID Connect acts as an extension to OAuth2. Implementations of this standard seem to vary, but the general idea is that the calling app requests an additional "openid" scope from the IdP. If granted, the IdP will issue a special ID token, encoded with useful information about the user, back to the app after a successful authorization is complete.
Under this approach, the app can avoid having to do additional lookups (using the access token) to the IdP in order to retrieve the required user profile information to log the user in.
SSO Tools supports OAuth2 with or without OpenID Connect.
A simplified OAuth2 flow
A user "logging-in" via OAuth2 will cause the following steps to occur.
- The user visits the app (e.g. a website or mobile app). The app shows an option to login via OAuth2 (e.g. "Login with Facebook" or "Login with Your App").
- The user selects this option and is redirected to the IdP (e.g. "Facebook" or "Your App") to authenticate themselves. Usually this would involve logging-in with a username and password. The app includes a list of scopes to ask for in its request.
- After a successful login, the IdP displays the scopes requested by the app, and asks the user to authorize the IdP to provide the requested information or permissions on the user's behalf.
- After authorization, the user is redirected back to the app armed with a special "authorization code".
- The app reads this code and uses this to request a token directly from the IdP.
- The IdP responds with an ID token (if the "openid" scope was requested) and - usually - an access token.
- The app can read the ID token to retrieve user details in order to match or setup an account for the user, and to log the user in. If an access token is also included, the app can use this for ongoing communication with the IdP if required.
Implementing the OAuth2 flow in your app
The following steps describe how to implement OAuth2 SSO in your app using an IdP, such as one provided by SSO Tools.
Step 1: Create an IdP and an app declaration
In SSO Tools, follow the steps to create a new IdP and app (service provider). In this guide we'll assume your IdP's issuer (code) is myidp.
When creating your app, you'll need to provide a "redirect URI". This the place the user will be redirected back to after successful authentication. If you don't know this right away, you can always change it later.
Step 1a: Choose how your app will prove its identity
Both options below use the same authorization code flow. They differ only in how your app proves, when it exchanges the code for a token, that it is the same app that started the flow.
Option 1: client secret. Your app sends the client secret issued by the IdP. This is the simplest approach, and it's appropriate when your app can genuinely keep a secret — a server-side web app, a backend service, or anything where the secret never leaves your own infrastructure.
Option 2: PKCE. Instead of a stored secret, your app invents a fresh random value for every login and proves it knows that value at the end. Use this for public clients — mobile apps, single-page apps, desktop and CLI tools — where any "secret" you ship can be extracted from the code by anyone who downloads it.
If you're unsure: if your app runs on a server you control, use Option 1. If it runs on someone else's device, use Option 2.
You can also use both together, which some confidential clients do for defence in depth.
The steps below are the same for both options; where they differ, each is called out.
Step 2: Redirect the user to the IdP to authenticate
In your app, create a button or link that redirects the user to the IdP's "authorization URL". In SSO Tools, these take the form of "https://idp.sso.tools/ISSUER/oauth2/authorize", replacing "ISSUER" with the issuer code for your IdP (e.g. "myidp").
Along with this request, you'll have to include some additional query parameters:
client_id: The client ID for the app generated by your IdP (this can be found on the IdP's connected apps page).scope: The space-separated list of requested permissions.redirect_uri: The URL to send the user back to after authentication. This must match the one declared in the app settings on the IdP.response_type: This should just be set to the value "code".
For the scope parameter, SSO Tools currently supports "email" (to allow reading the user's email address), "profile" (to allow reading the user's name and profile information), "openid" (to receive an ID token after authentication), and "offline_access" (to receive a refresh token for long-lived API access).
As such, an example request to redirect the user to on SSO Tools could look like the following:
https://idp.sso.tools/myidp/oauth2/authorize?client_id=abc123&scope=profile%20email%20openid&redirect_uri=https://myapp.com/oauth/callback&response_type=code
If you're using PKCE (Option 2)
Before redirecting, generate a code verifier: a fresh random string between 43 and 128 characters, made up of letters, digits and the characters -._~. Keep it somewhere your app can retrieve it later in the flow (a session, or secure storage on the device) — you'll need it in Step 5.
Then derive the code challenge from it. With the recommended S256 method the challenge is the base64url encoding of the SHA-256 hash of the verifier, with no padding:
code_challenge = base64url( SHA-256( code_verifier ) )
In Node.js, for example:
import crypto from 'crypto'
const codeVerifier = crypto.randomBytes(32).toString('base64url')
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url')
Add two more query parameters to the authorization request:
code_challenge: The challenge derived above.code_challenge_method:S256. (Theplainmethod, which sends the verifier itself as the challenge, is also accepted but offers no protection if the request is intercepted — preferS256.)
Giving a request like this:
https://idp.sso.tools/myidp/oauth2/authorize?client_id=abc123&scope=profile%20email%20openid&redirect_uri=https://myapp.com/oauth/callback&response_type=code&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256
Note that once you include a code_challenge, the IdP requires the matching code_verifier at Step 5 — sending a client secret instead will not work. That's deliberate: it's what stops somebody who intercepts your authorization code from redeeming it.
Step 3: The user authenticates
Your app can relax for a bit now, since the IdP now takes over in handling the user's authentication and seeking approval for the scopes your app has requested.
Step 4: Receive the user back after authentication
Assuming all went well with the authentication, the IdP will now redirect the user back to your app at the redirect URI you specified. If you don't yet have a route to handle this, then you should create one now and ensure this is registered in the app on SSO Tools and included in your request in Step 2.
When the user arrives back, they'll have a code parameter in their request URL. For example, if your redirect URI is "https://myapp.com/oauth/callback" your app will receive a request at "https://myapp.com/oauth/callback?code=xzy321"
Your app should read this code and make it available for the next step
Step 5: Request a token from the IdP
Next, your app will need to make a request directly to the IdP on its "token" endpoint. On SSO Tools, these endpoints look like "https://idp.sso.tools/ISSUER/oauth2/token". To this endpoint, your app should send an HTTP POST request containing the following data in JSON format:
code: The code received in Step 4.client_id: The client ID (as shown on the connected apps page).redirect_uri: The same redirect URI we used earlier, again.grant_type: This should be set to the value "authorization_code".
Plus one more field, depending on which option you chose in Step 1a.
Option 1: with a client secret
client_secret: The client secret (as shown on the connected apps page).
POST https://idp.sso.tools/myidp/oauth2/token
Accept: application/json
Content-Type: application/json
{
"code": "xyz321",
"client_id": "abc123",
"client_secret": "SECRET",
"redirect_uri": "https://myapp.com/oauth/callback",
"grant_type": "authorization_code"
}
Option 2: with PKCE
code_verifier: The verifier you generated in Step 2 — the original random string, not the challenge you derived from it.
POST https://idp.sso.tools/myidp/oauth2/token
Accept: application/json
Content-Type: application/json
{
"code": "xyz321",
"client_id": "abc123",
"code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
"redirect_uri": "https://myapp.com/oauth/callback",
"grant_type": "authorization_code"
}
The IdP hashes the verifier you send and checks it against the challenge from Step 2. If they don't match — or if you started the flow with a code_challenge and then don't send a verifier — the request is rejected.
Assuming all went well, the response to the request should contain an access_token field (containing a string representing an access token usable against the SSO Tools API for the IdP). If the "openid" scope was requested the response will also include an "id_token" field (containing a string representing a JWT that can be decoded to retrieve the user profile information requested by the scope). If the "offline_access" scope was requested, the response will also include a "refresh_token" field for obtaining new access tokens without re-authentication.
Step 6 (optional): Interact with the IdP's API
Depending on your setup, Step 5 might be the last step needed to complete single sign-on with OAuth2 and OpenID Connect. If your app needs to then communicate with an API for additional data flow (e.g. to power an integration), then read on.
The "access_token" received in Step 5 can now be used against the IdP's API in order to retrieve the user's information. To do so, include the access token in the Authorization header and send off a request to "https://idp.sso.tools/ISSUER/api/users/me".
For example, given the above information, your request could look like this:
GET https://idp.sso.tools/myidp/api/users/me
Accept: application/json
Authorization: ACCESSTOKEN
Assuming all went well, the IdP will respond with information about the current user, as defined in the requested scopes.
Step 7 (optional): Maintain long-lived sessions with refresh tokens
The access token issued in Step 5 is valid for 1 hour. After this, it will no longer be accepted by the IdP's API. If your app needs to maintain access without requiring the user to re-authenticate, you can request a refresh token by including the offline_access scope in your initial authorization request.
When the offline_access scope is included, the token response (Step 5) will also contain a refresh_token field. Refresh tokens are valid for 30 days and can be used to obtain new access tokens without user interaction.
To refresh an access token, make a POST request to the token endpoint with the following data:
client_id: The client ID for your app.client_secret: The client secret for your app.refresh_token: The refresh token received in the previous token response.grant_type: Set to"refresh_token".
Note that unlike the authorization code exchange, refreshing always requires the client secret — PKCE does not apply to this grant. A public client that cannot safely hold a secret should therefore treat access tokens as short-lived and send the user back through Step 2 when one expires, rather than requesting offline_access.
For example:
POST https://idp.sso.tools/myidp/oauth2/token
Accept: application/json
Content-Type: application/json
{
"client_id": "abc123",
"client_secret": "SECRET",
"refresh_token": "REFRESH_TOKEN_HERE",
"grant_type": "refresh_token"
}
The response will contain a new access_token (valid for 1 hour) and a new refresh_token (valid for 30 days). You should store the new refresh token for use in your next refresh cycle. Each time you use a refresh token, it is replaced with a new one for security purposes.
Step 8 (optional): Revoke tokens
If your app needs to explicitly invalidate a token (for example, when a user disconnects or logs out of your app), you can revoke it using the revocation endpoint. This will immediately invalidate the token so it can no longer be used to access the IdP's API.
To revoke a token, make a POST request to the revocation endpoint with the token in the request body:
token: The access token or refresh token you want to revoke.
For example:
POST https://idp.sso.tools/myidp/oauth2/revoke
Accept: application/json
Content-Type: application/json
{
"token": "ACCESS_OR_REFRESH_TOKEN_HERE"
}
You can also pass the token in the Authorization header as a Bearer token instead of in the body. The endpoint will respond with an empty JSON object on success. Revoking an access token also invalidates the associated refresh token, and vice versa.