Single Sign-On (SSO) Integration Guide

For: Developers building applications that sign users in with their Audit Scorecard account Provider: Audit Scorecard (OpenID Connect identity provider) Document version: 1.0


Table of contents

  1. Overview
  2. Prerequisites
  3. Endpoints
  4. Sign-in flow
  5. Implementation steps
  6. Scopes and claims
  7. Token reference
  8. Quick start — Node.js
  9. Quick start — Laravel / PHP
  10. Access control and revocation
  11. Error handling
  12. Security checklist
  13. FAQ
  14. Protocol references

1. Overview

Audit Scorecard is the identity provider and the single source of truth for user accounts. Your application is the relying party.

A user signs in with their existing Audit Scorecard account and arrives in your application already authenticated. Your application never sees, requests, or stores their password.

The integration uses standard OpenID Connect (OAuth 2.0 authorization code grant with PKCE). Any conformant OIDC client library will work — you do not need custom crypto or custom endpoints.

What you are responsible for

Concern Owner
Password storage and authentication Audit Scorecard
Identity verification (id_token signature) Audit Scorecard signs; you verify
Session management in your app You
Application authorization (who can do what) You
Access scoping to Audit Scorecard data You (see the separate Integration API)

Note SSO handles identity only. Audit Scorecard verifies who the user is; your application decides what they are permitted to do. To read data from Audit Scorecard (projects, audits, teams), that is a separate mechanism — the Integration API, documented separately.


2. Prerequisites

Before you begin you need from your Audit Scorecard administrator:

Item Description
Issuer URL The base URL of the Audit Scorecard instance
Client ID Issued when your application is registered
Client secret Confidential clients only. Displayed once — store it securely
Redirect URI(s) Exact callback URL(s) on your application
Approved scopes Which of openid, profile, email, roles you may request

Client types

Type Use for Secret PKCE
Confidential Server-side apps (Node, Laravel, Django…) Yes Recommended
Public Single-page apps, mobile, desktop No Mandatory

Discovery check

Confirm the provider is reachable before writing code:

curl https://YOUR_ISSUER/.well-known/openid-configuration

If this returns a JSON document, the provider is live and you can proceed.


3. Endpoints

All endpoints are derived from the issuer URL. Most OIDC libraries fetch this automatically via discovery — you normally only need the discovery URL.

Purpose Endpoint
Discovery {ISSUER}/.well-known/openid-configuration
Authorization {ISSUER}/oauth/authorize
Token {ISSUER}/oauth/token
Userinfo {ISSUER}/oauth/userinfo
JWKS {ISSUER}/oauth/jwks

Provider capabilities

As advertised in the discovery document:

Property Value
response_types_supported code
grant_types_supported authorization_code, refresh_token
subject_types_supported public
id_token_signing_alg_values_supported RS256
token_endpoint_auth_methods_supported client_secret_basic, client_secret_post, none
code_challenge_methods_supported S256, plain
scopes_supported openid, profile, email, roles

Use S256 for PKCE. Although plain is advertised, it should not be used.


4. Sign-in flow

###4.1 Happy path

[Diagram]

4.2 Token lifecycle

[Diagram]

4.3 Decision flow

[Diagram]

5. Implementation steps

Step 1 — Register your application

Provide your administrator with:

The redirect URI must match exactly: scheme, host, port, path, and trailing slash. A mismatch is the single most common cause of a failed redirect.

Example: https://partner-tool.example.com/auth/callback Not: https://partner-tool.example.com/auth/callback/

Step 2 — Install an OIDC client library

Stack Library
Node.js openid-client
Laravel / PHP league/oauth2-client
Python authlib or requests-oidc
Java / Spring spring-boot-starter-oauth2-client
.NET IdentityModel / Duende
React SPA oidc-client-ts

Only openid-client (Node) and league/oauth2-client (PHP) are shown in this guide.

Step 3 — Implement the redirect to sign-in

When the user clicks your sign-in button:

  1. Generate a cryptographically random code_verifier (43–128 characters).
  2. Derive code_challenge = BASE64URL(SHA256(code_verifier)).
  3. Generate a random state and nonce.
  4. Store code_verifier, state, and nonce in the user's session.
  5. Redirect the browser to the authorization endpoint.
GET {ISSUER}/oauth/authorize?
    response_type=code&
    client_id=YOUR_CLIENT_ID&
    redirect_uri=https://partner-tool.example.com/auth/callback&
    scope=openid profile email roles&
    state=RANDOM_STATE&
    code_challenge=BASE64URL_SHA256_OF_VERIFIER&
    code_challenge_method=S256&
    nonce=RANDOM_NONCE

state and nonce protect against CSRF and token replay. Both must be verified on return.

Step 4 — Handle the callback

  1. Verify state matches the value stored in session. Reject immediately if not.
  2. Exchange the code for tokens.
  3. Verify the id_token or fetch claims from userinfo.
  4. Find-or-create your local user keyed by sub.
  5. Create your local session and redirect into the application.

Step 5 — Exchange the code for tokens

curl -X POST {ISSUER}/oauth/token \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d grant_type=authorization_code \
  -d client_id=YOUR_CLIENT_ID \
  -d client_secret=YOUR_CLIENT_SECRET \
  -d redirect_uri=https://partner-tool.example.com/auth/callback \
  -d code=AUTHORIZATION_CODE \
  -d code_verifier=ORIGINAL_PKCE_VERIFIER

Public clients omit client_secret.

Step 6 — Identify the user

Verify the id_token locally against the JWKS, or call userinfo with the access token.

ID token

{
  "iss": "https://YOUR_ISSUER",
  "sub": "42",
  "aud": "YOUR_CLIENT_ID",
  "iat": 1750000000,
  "exp": 1750003600,
  "auth_time": 1750000000,
  "nonce": "RANDOM_NONCE",
  "name": "Grace Hopper",
  "given_name": "Grace",
  "family_name": "Hopper",
  "email": "grace@example.com",
  "email_verified": true,
  "roles": ["Team Manager", "Auditor"]
}

Userinfo request

curl {ISSUER}/oauth/userinfo \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'Accept: application/json'

Step 7 — Refresh tokens

curl -X POST {ISSUER}/oauth/token \
  -H 'Accept: application/json' \
  -d grant_type=refresh_token \
  -d client_id=YOUR_CLIENT_ID \
  -d client_secret=YOUR_CLIENT_SECRET \
  -d refresh_token=REFRESH_TOKEN

Refresh early — proactively renew rather than waiting for a 401.


6. Scopes and claims

Request only the scopes you need. Each scope is approved individually.

Scope Claims returned Purpose
openid sub Required. Stable user identifier
profile name, given_name, family_name, picture Display name and avatar
email email, email_verified Email address
roles roles Active Audit Scorecard role names

ID token envelopePresent in addition to the claims above.

Claim Description
iss Issuer — must equal {ISSUER}
sub Subject — the Audit Scorecard user ID, as a string
aud Audience — your client ID
iat Issued-at (Unix timestamp)
exp Expiry (Unix timestamp)
auth_time When the user authenticated
nonce Echoes your nonce — present only when PKCE is used

Use sub as your primary key. sub is stable for the lifetime of the account. Email addresses can change; never key your local user table on email alone.

About the roles claim

roles contains the user's active Audit Scorecard role names as an array of strings. It reflects identity only — it is not a permission grant to your system.

Use it to map Audit Scorecard roles onto your own authorization model. Your application remains responsible for enforcing access.


7. Token reference

Token Format Lifetime Purpose
Authorization code Opaque Very short Single-use credential returned to your redirect URI
Access token JWT Short (~1 hour) Bearer credential for /oauth/userinfo
ID token JWT (RS256) Matches access token Signed identity claims

Verifying the ID token

[Diagram]

Cache the JWKS. Do not fetch it on every request. Refresh only when you encounter an unknown kid (key rotation).

Standard OIDC libraries perform all of these checks automatically. Do not hand-roll them.


8. Quick start — Node.js

Install

npm i openid-client express-session

Full example

import express from 'express';
import session from 'express-session';
import { Issuer, generators } from 'openid-client';

const app = express();
app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: true }));

// Discover once at startup
const issuer = await Issuer.discover(
  `${process.env.AUDIT_SSO_HOST}/.well-known/openid-configuration`
);

const client = new issuer.Client({
  client_id: process.env.AUDIT_SSO_CLIENT_ID,
  client_secret: process.env.AUDIT_SSO_CLIENT_SECRET,
  redirect_uris: ['https://partner-tool.example.com/auth/callback'],
  response_types: ['code'],
});

// --- GET /login : send the user to Audit Scorecard ---
app.get('/login', (req, res) => {
  const code_verifier = generators.codeVerifier();
  const code_challenge = generators.codeChallenge(code_verifier);
  const nonce = generators.nonce();
  const state = generators.state();

  req.session.oidc = { code_verifier, nonce, state };

  res.redirect(client.authorizationUrl({
    scope: 'openid profile email roles',
    code_challenge,
    code_challenge_method: 'S256',
    state,
    nonce,
  }));
});

// --- GET /auth/callback : exchange code and read the user ---
app.get('/auth/callback', async (req, res) => {
  try {
    const params = client.callbackParams(req);

    // Validates state, PKCE, nonce, signature, iss, aud, exp
    const tokenSet = await client.callback(
      'https://partner-tool.example.com/auth/callback',
      params,
      {
        code_verifier: req.session.oidc.code_verifier,
        state: req.session.oidc.state,
        nonce: req.session.oidc.nonce,
      }
    );

    const claims = tokenSet.claims();   // { sub, name, email, roles, ... }

    // find-or-create your local user keyed by `sub`
    req.session.user = {
      sub: claims.sub,
      name: claims.name,
      email: claims.email,
      roles: claims.roles ?? [],
    };

    delete req.session.oidc;
    res.redirect('/');
  } catch (err) {
    console.error('OIDC callback failed:', err);
    res.redirect('/login?error=sso_failed');
  }
});

app.listen(3000);

Verifying manually (optional)

If you prefer the claims endpoint over local verification:

const userinfo = await client.userinfo(tokenSet.access_token);
// { sub, name, given_name, family_name, email, email_verified, roles }

9. Quick start — Laravel / PHP

Install

composer require league/oauth2-client

.env

AUDIT_SSO_HOST=https://YOUR_ISSUER
AUDIT_SSO_CLIENT_ID=your-client-id
AUDIT_SSO_CLIENT_SECRET=your-client-secret
AUDIT_SSO_REDIRECT=https://partner-tool.example.com/auth/callback

config/services.php

'audit_sso' => [
    'host'          => env('AUDIT_SSO_HOST'),
    'client_id'     => env('AUDIT_SSO_CLIENT_ID'),
    'client_secret' => env('AUDIT_SSO_CLIENT_SECRET'),
    'redirect'      => env('AUDIT_SSO_REDIRECT'),
],

Controller

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use League\OAuth2\Client\Provider\GenericProvider;

class SsoController extends Controller
{
    private function provider(): GenericProvider
    {
        $host = config('services.audit_sso.host');

        return new GenericProvider([
            'clientId'                => config('services.audit_sso.client_id'),
            'clientSecret'            => config('services.audit_sso.client_secret'),
            'redirectUri'             => config('services.audit_sso.redirect'),
            'urlAuthorize'            => $host.'/oauth/authorize',
            'urlAccessToken'          => $host.'/oauth/token',
            'urlResourceOwnerDetails' => $host.'/oauth/userinfo',
            'scopes'                  => ['openid', 'profile', 'email', 'roles'],
            'scopeSeparator'          => ' ',
            'pkceMethod'              => GenericProvider::PKCE_METHOD_S256,
        ]);
    }

    /** GET /login — redirect the user to Audit Scorecard */
    public function redirect()
    {
        $provider = $this->provider();
        $url = $provider->getAuthorizationUrl();

        session([
            'sso_state'  => $provider->getState(),
            'sso_pkce'   => $provider->getPkceCode(),
            'sso_nonce'  => $provider->getNonce(),
        ]);

        return redirect($url);
    }

    /** GET /auth/callback — handle the response */
    public function callback(Request $request)
    {
        //1. Reject CSRF attempts
        abort_if(
            $request->input('state') !== session('sso_state'),
            403,
            'Invalid state'
        );

        // 2. Exchange the code
        $provider = $this->provider();
        $provider->setPkceCode(session('sso_pkce'));

        try {
            $token = $provider->getAccessToken('authorization_code', [
                'code' => $request->input('code'),
            ]);
        } catch (\Throwable $e) {
            report($e);
            return redirect('/login?error=sso_failed');
        }

        // 3. Read the verified claims
        $claims = $provider->getResourceOwner($token)->toArray();
        // $claims['sub'], $claims['email'], $claims['name'], $claims['roles']

        // 4. Find-or-create your local user, then authenticate
        $user = User::firstOrCreate(
            ['sso_sub' => $claims['sub']],
            ['name' => $claims['name'] ?? null, 'email' => $claims['email'] ?? null]
        );

        Auth::login($user, remember: true);
        $request->session()->regenerate();

        return redirect()->intended('/dashboard');
    }
}

Verify claims. league/oauth2-client does not validate the id_token JWT signature by default when reading claims from userinfo. Add a resource-server (e.g. firebase/php-jwt) or call getParsedResponse() with a JwtDecoder and verify against the JWKS. Treat the access token as a server-to-server credential over TLS and never trust unverified ID token contents.


10. Access control and revocation

Your application's registration may be linked to an Audit Scorecard application record. When it is, the following rules are enforced live — not only at first sign-in.

Control Effect
Application status A suspended or expired application stops all sign-ins and token use immediately
Allowed scopes Narrowing the permitted scopes takes effect on tokens that already exist
Sign-in allow-list Users or roles not on the list are refused, including mid-session refresh
Account status A deactivated user cannot sign in or refresh

Consequence for your application

A refresh or token exchange can fail after working normally. This is intentional.

[Diagram]

On any token failure, discard your stored tokens and re-run sign-in. Do not retry a refresh token that the provider has refused.


11. Error handling

Errors on the redirect to your application

Your redirect_uri receives an error query parameter:

Parameter Meaning What to do
error=access_denied The user or application is not permitted to sign in Show a clear message; do not loop
error=invalid_scope A requested scope is not approved Remove the scope and retry only if appropriate
error=login_required No active session at the provider Re-run sign-in
error=consent_required Consent is required Re-run sign-in with consent
error=invalid_request Malformed request Check client_id, redirect_uri, response_type
error=server_error Provider-side fault Retry with backoff; contact support if persistent

The original state is preserved on error redirects. Always validate it.

Errors at the token endpoint

Status Error Meaning What to do
400 invalid_grant Code or refresh token refused — user or application no longer authorized Discard tokens, re-run sign-in
400 invalid_scope A requested scope is no longer approved Contact your administrator
401 invalid_client Client ID or secret is wrong Verify credentials
400 invalid_request Missing or malformed parameter Check the request

Errors at userinfo

Status Error Meaning What to do
401 invalid_token Bearer token invalid, expired, or the user is no longer authorized Discard tokens, re-run sign-in
403 insufficient_scope Token lacks the openid scope Request openid during sign-in

Troubleshooting map

[Diagram]

12. Security checklist

Before going to production, confirm each item.

# Check Why
1 redirect_uri is registered and matched exactly Prevents open redirect and code theft
2 state generated, stored in session, and verified on return CSRF protection
3 nonce generated, stored, and verified in the ID token Replay protection
4 PKCE with code_challenge_method=S256 on every flow Code interception protection
5 id_token signature verified against the JWKS Prevents forged identity
6 iss, aud, and exp all validated Prevents replay and cross-client attacks
7 JWKS cached; refetched only on unknown kid Correct handling of key rotation
8 Client secret stored in a secret manager, never in source control Credential exposure
9 Local user keyed by sub, not email Survives email changes
10 Session ID regenerated after authentication Prevents session fixation
11 Tokens never written to logs Prevents credential leakage
12 HTTPS on every endpoint Baseline transport security

13. FAQ

Q. Do Audit Scorecard permissions transfer to my application?

Only role names are shared, via the roles scope. Audit Scorecard verifies identity; your application owns its own authorization. Map the role names onto your own permission model.

Q. Is PKCE required?

Mandatory for public clients and strongly recommended for all clients. Send code_challenge and code_challenge_method=S256. The nonce is only echoed into the id_token when PKCE is used, so you should always use it.

Q. Is there single logout?

Not yet. Your application manages its own session. Logging out of Audit Scorecard ends only the local session there; connected tools remain signed in until their tokens expire. Use refresh_token, or send the user back through the sign-in flow. Plan for a sign-out redirect in your application instead of relying on provider-side logout.

Q. The redirect returns an error. What should I check?

In order: (1) redirect_uri matches a registered value exactly, including trailing slash; (2) client_id is correct; (3) scope includes openid; (4) state matches your session.

Q. Why is id_token missing from the token response?

The openid scope was not granted. Request it explicitly — it is not implied.

Q. Why is nonce missing from my ID token?

nonce is only echoed when PKCE is used. Add code_challenge / code_challenge_method=S256 to your authorization request.

Q. How long are tokens valid?

Access and ID tokens are short-lived (approximately one hour). Refresh tokens are long-lived. Refresh proactively rather than waiting for a 401.

Q. Can I sign in users from my own login page?

Yes, through the Integration API and MCP server, which use bearer tokens rather than a user session. This guide covers interactive sign-in only.

Q. How do I get user data such as projects or audits?

Through the Integration API — a separate REST interface authenticated with a bearer token. It is documented separately and is not part of this SSO flow.

Q. What happens if my application's access is revoked?

The next token exchange or userinfo call fails with invalid_grant or invalid_token. Discard your stored tokens, clear the local session, and re-run sign-in. See section 10.

Q. Who do I contact for support?

Your Audit Scorecard administrator, for client credentials, scope approvals, and access revocation. For IdP availability, contact your internal support channel.

Q. Is there a sandbox environment?

Contact your Audit Scorecard administrator to request a non-production instance for testing.


14. Protocol references

Specification Relevance
OpenID Connect Core 1.0 id_token, claims, the authentication flow
OpenID Connect Discovery 1.0 The .well-known/openid-configuration document
RFC 6749 OAuth 2.0 authorization framework
RFC 7636 Proof Key for Code Exchange (PKCE)
RFC 6750 Bearer token usage
RFC 7519 JSON Web Token (JWT)
RFC 7517 JSON Web Key Set (JWKS)

Audit Scorecard is the single source of truth for user identity. This guide covers authentication only; authorization and data access remain your application's responsibility.