Ops Did I just Commit That?

A gate of silence waits ahead,

no key, no plea, no hurried tread— but speak the truth, the name you own, and paths once barred become your own.

Table of Contents

Introduction

OpenID Connect (OIDC) is an interoperable authentication protocol built as a simple identity layer on top of OAuth 2.0 (OpenID Connect authentication with Microsoft Entra ID - Microsoft Entra | Microsoft Learn) (How OpenID Connect Works - OpenID Foundation). In essence, OIDC allows one application (the Relying Party, or client) to outsource user authentication to a trusted Identity Provider (IdP) and receive proof of the user’s identity in return. This means your application doesn’t have to manage or verify passwords itself – instead, the IdP handles that and issues tokens confirming who the user is. The design goal of OIDC is often quoted as “making simple things simple and complicated things possible” (OpenID Connect authentication with Microsoft Entra ID - Microsoft Entra | Microsoft Learn). By using OIDC for login, developers get a secure way to verify user identities across websites and apps without managing credentials, enabling features like single sign-on (SSO) across multiple applications (OpenID Connect authentication with Microsoft Entra ID - Microsoft Entra | Microsoft Learn) (How OpenID Connect Works - OpenID Foundation).

Why use OpenID Connect? For users, it means they can log into different services with one central account (for example, using their Google or Azure AD account). For developers, it means less worry about storing passwords and implementing secure login flows from scratch. Instead, the IdP (be it a third-party like Google/Microsoft or a self-hosted service like Authelia) handles authentication and issues a JSON Web Token (JWT) that the application can trust. OIDC improves security by leveraging established identity providers – “putting the responsibility for user identity verification in the hands of expert providers” (How OpenID Connect Works - OpenID Foundation) – and by using strong cryptographic tokens (JWTs) instead of traditional session IDs.

In this guide, we will focus on implementing OpenID Connect authentication with Authelia as the primary OpenID Provider (OP). Authelia is an open-source authentication/authorization server that supports acting as an OpenID Connect 1.0 provider (OpenID Connect 1.0 | Overview | Authelia), allowing you to self-host your identity provider much like you would with Google or Azure. We’ll walk through the entire flow of a user logging in via Authelia (and how the front-end and back-end of your application should handle it), provide code snippets for each step, and discuss important details like JWT handling and security best practices. We’ll also note how this would similarly apply if using Google or Azure AD as the OIDC provider (since they adhere to the same OIDC standards (How to configure SSO with OpenID Connect in the Legacy interface – TalentLMS Support - Help Center) (OpenID Connect authentication with Microsoft Entra ID - Microsoft Entra | Microsoft Learn)).

Note: OpenID Connect extends OAuth 2.0 by adding an identity layer. If you’re familiar with OAuth flows, OIDC’s flows will look very similar – the key difference is the inclusion of an ID Token (a JWT with user identity claims) in addition to the usual OAuth Access Token (An Illustrated Guide to OAuth and OpenID Connect | Okta Developer). As we proceed, keep in mind that Authelia will be our Identity Provider (like Google/Microsoft in a typical OAuth/OIDC scenario), and our application’s back-end will be the client (OIDC Relying Party) that trusts Authelia to authenticate users.

Authentication Flow

At a high level, the OIDC authentication process with Authelia involves a series of redirects and token exchanges between the Frontend, Backend, and Authelia (IdP). It can be summarized as: Frontend → Backend → Authelia → Backend → Frontend. Let’s break that down into steps and explain each in plain language (with a real-world analogy), then show what needs to happen under the hood (with code snippets):

1. Frontend Initiates Login (Frontend → Backend)

Imagine a user trying to access a members-only page on your site. The frontend (browser) sends a request to your backend for that protected resource. The backend checks if the user is already authenticated (e.g. has a session or valid token). If not, it needs to start the login process.

Analogy: The user walks up to a club’s entrance (the backend) without a stamp on their hand (no valid session). The bouncer (backend) sees no proof of entry and says “you need to get a stamp from the registration desk to come in.”

StepActionDescription (Analogy)
1Frontend → BackendUser wants to enter a club (the protected app), but the bouncer (backend) notices they don’t have an entry stamp (no valid session).
2Backend → AutheliaBouncer directs them to the registration desk (Authelia) to get proper identification and an entry stamp.

What the Backend Does: Instead of serving the requested page, the backend will respond with a redirect to the IdP (Authelia) login page. This redirect is an OIDC Authorization Request telling Authelia who is requesting authentication and what is being asked for.

// Pseudocode (Node/Express style) for an endpoint that starts login
app.get('/login', (req, res) => {
  // Construct the authorization request URL for Authelia
  const state = generateRandomState(); // to correlate response
  req.session.oidcState = state; // save state to verify later

  const authUrl =
    `${AUTHELIA_BASE_URL}/api/oidc/authorization` +
    `?response_type=code&client_id=${CLIENT_ID}` +
    `&redirect_uri=${REDIRECT_URI}` +
    `&scope=openid+profile+email` +
    `&state=${state}`;
  res.redirect(authUrl);
});

In this snippet, the backend generates a URL to Authelia’s authorization endpoint (as discovered from Authelia’s OIDC config, typically at /api/oidc/authorization (OpenID Connect 1.0 | Integration | Authelia)). It includes parameters like client_id (identifying our application), redirect_uri (where Authelia should send the user back after login), response_type=code (we want an authorization code flow), scope=openid profile email (asking for basic OIDC info), and a random state string for security. The backend then issues a redirect (HTTP 302) to this URL, sending the user’s browser to Authelia.

2. User Authenticates with Authelia (Backend → Authelia)

Now the browser navigates to Authelia’s site (the IdP) with the authorization request. Authelia will present a login screen if the user isn’t already logged in there. The user enters their credentials (e.g., username/password, and possibly 2FA if Authelia is configured for two-factor). Authelia verifies the credentials against its user database (could be LDAP, file, etc.), and if successful, the user is authenticated at Authelia.

StepActionDescription (Analogy)
3Frontend → AutheliaUser goes to the registration desk (Authelia) and shows their ID (logs in with username/password). The attendant (Authelia) confirms the user’s identity – perhaps even doing a second check like asking for a code texted to their phone (2FA) – and then prepares a stamped ticket (an authorization code).
4Authelia → BackendBouncer hands over the stamped ticket (authorization code) to the security guard (backend).

What Authelia Does: Upon successful login, Authelia will generate an authorization code, a short-lived one-time code representing the user’s authenticated session. Authelia then redirects the browser back to the redirect_uri that was provided (which points to our backend), including this code as a query parameter. For example, it might redirect to:

https://your-app.com/oauth2/callback?code=SplxlOExampleCode&state=xyz123

At this point, the user’s browser is heading back to our application’s backend, carrying the code and the state.

3. Handling the Callback (Authelia → Backend)

The browser hits our backend’s callback endpoint (e.g. /oauth2/callback) with the code (and the state). This is an important step: our backend must verify that the state matches what it set in Step 1 (to ensure this response is not some unsolicited or replayed response) (Purpose of state and nonce in OpenID Connect Code flow - Stack Overflow). Assuming the state checks out, the backend will proceed to exchange the authorization code for tokens.

StepActionDescription (Analogy)
5Authelia → BackendThe user returns to the club entrance with a stamped ticket from the desk. (Authelia Redirects to Callback url)
6Backend → FrontendThe bouncer checks that the ticket has the correct random serial number he issued (state) – this ensures the user who left is the same who came back, preventing a scenario where someone else intercepts or reuses the ticket. Everything looks good, so now the bouncer will verify the ticket’s validity. and lets the user in.

Code (Backend Callback Handling):

app.get('/oauth2/callback', async (req, res) => {
  const { code, state: returnedState } = req.query;
  // Verify the state matches
  if (!code || returnedState !== req.session.oidcState) {
    return res.status(400).send('Invalid OIDC state or missing code');
  }
  // Exchange the code for tokens
  try {
    const tokenResponse = await axios.post(
      `${AUTHELIA_BASE_URL}/api/oidc/token`,
      {
        grant_type: 'authorization_code',
        code: code,
        redirect_uri: REDIRECT_URI,
        client_id: CLIENT_ID,
        client_secret: CLIENT_SECRET,
      }
    );
    const { id_token, access_token, refresh_token } = tokenResponse.data;
    // Verify and decode the ID token (see next section)
    const userClaims = verifyIDToken(id_token);
    // Establish session (e.g., set cookie or store session data)
    req.session.user = userClaims;
    res.redirect('/'); // login successful, redirect to home or original page
  } catch (err) {
    console.error('Token exchange failed:', err);
    res.status(500).send('Authentication failed');
  }
});

Here the backend receives the auth code, checks state (Purpose of state and nonce in OpenID Connect Code flow - Stack Overflow), then makes a back-channel HTTP POST to Authelia’s token endpoint (/api/oidc/token (OpenID Connect 1.0 | Integration | Authelia)). It sends the code, along with the client_id, client_secret (since our backend is a confidential client), and the same redirect_uri (the token endpoint needs to confirm the code is being redeemed by the same client and redirect URI that requested it). Authelia will validate the code and respond with an ID Token (a JWT), an Access Token, and possibly a Refresh Token (if our scope and client settings allow it, e.g. if we requested offline_access scope).

4. Issuing Tokens and Logging In (Backend)

Once the backend has the tokens from Authelia, it needs to verify the ID Token and then use the information inside it to log the user into the application.

Authelia signs the ID Token as a JWT (usually with its RSA private key using RS256) (OpenID Connect 1.0 Provider | Configuration | Authelia) (OpenID Connect 1.0 Clients | Configuration | Authelia). Our backend must validate this token – typically by checking the signature (using Authelia’s public key), the iss (issuer) and aud (audience) claims, and ensuring it hasn’t expired. We’ll dive deeper into JWT structure in the next section, but for now, assume verifyIDToken(id_token) handles these checks and returns the decoded payload (user identity claims).

Analogy: The bouncer examines the stamp/ticket closely to ensure it’s genuine and issued for this club (checks the signature/issuer) and that it hasn’t expired (some tickets might only be valid for a short time). Once verified, the bouncer now knows the person’s identity and that they are allowed in.

After verification, the backend can consider the user authenticated. It might create an application session (for example, storing the user’s info in a server-side session store and setting a session cookie). In a modern SPA scenario, the backend might instead forward the tokens to the frontend to store and use for API calls – but often for web apps, a secure HTTP-only session cookie is used at this point for simplicity.

Finally, the backend redirects the user to the originally requested page or a post-login landing page. Now, when the frontend requests a protected resource again, the backend sees an active session or valid token for the user and grants access.

Summary of the Flow: In OIDC terms, what we described is the Authorization Code Flow: the frontend was sent to Authelia to authenticate, Authelia returned an auth code to our backend, the backend exchanged it for an ID Token and possibly other tokens, and then the user got logged in. All of this might involve a few HTTP redirects but happens very quickly. The user effectively went to Authelia’s domain and back, and now has a valid login session on our app.

Implementation Details

Now that we’ve walked through the flow conceptually, let’s get into the specifics of implementing this. There are a few pieces to set up:

1. Setting up Authelia as an OpenID Provider: First, Authelia itself needs to be configured to operate as an OIDC identity provider. In Authelia’s configuration file (usually configuration.yml), you’ll need to enable the identity_providers.oidc section and provide the necessary settings (keys, secrets, etc.). At a high level, you must:

  • Enable OIDC and Provide Signing Keys: Authelia requires a signing key to issue JWTs. In the config, you’ll generate or supply a JSON Web Key Set (JWKS). For example, a minimal Authelia config might include an RSA key for signing tokens and an HMAC secret for token HMAC operations:

    identity_providers:
      oidc:
        hmac_secret: 'super_long_random_secret_here'
        jwks:
          - key_id: 'authelia-signing-key'
            algorithm: 'RS256'
            use: 'sig'
            key: |
              -----BEGIN RSA PRIVATE KEY-----
              (your RSA private key)
              -----END RSA PRIVATE KEY-----
    

    This tells Authelia to sign ID Tokens using RS256 with the given RSA key (OpenID Connect 1.0 Provider | Configuration | Authelia). (Authelia will automatically serve the corresponding public key via its JWKS endpoint for clients to verify signatures.)

  • Register OIDC Clients: You need to inform Authelia about the applications that will use it for login. Each application (client) gets a client_id (and for confidential clients, a client_secret). You also configure allowed redirect URIs, scopes, and other OIDC settings for that client. In configuration.yml under oidc: clients:, you would add an entry for your application. For example:

    identity_providers:
      oidc:
        clients:
          - client_id: 'my-app-client-id'
            client_name: 'My Web Application'
            client_secret: '$pbkdf2-sha512$...$JNRBzwAo...$...$...' # hashed secret
            redirect_uris:
              - 'https://myapp.example.com/oauth2/callback'
            scopes:
              - openid
              - email
              - profile
            grant_types:
              - authorization_code
              - refresh_token
            response_types:
              - code
            # ... other settings ...
    

    In this snippet, we register a client with ID "my-app-client-id" and allowed to use the authorization code flow (with optional refresh tokens) (OpenID Connect 1.0 Clients | Configuration | Authelia) (OpenID Connect 1.0 Clients | Configuration | Authelia). The redirect_uris must match exactly the URL our backend will use to receive the auth code. We also list the scopes this client can ask for (at minimum, openid is required for OIDC). The client_secret is stored as a hashed value in Authelia’s config (Authelia provides a command or documented method to generate this hash for a given plaintext secret) (Frequently Asked Questions | Integration - Authelia). Authelia can enforce policies per client, such as requiring 2FA (authorization_policy) or PKCE. Ensure the issuer URL (the public URL where users reach Authelia) is correctly configured, as it will be used in tokens and discovery documents.

  • Authelia Discovery Document: Once configured and running, Authelia exposes a discovery document at /.well-known/openid-configuration (OpenID Connect 1.0 | Integration | Authelia) (e.g., https://auth.example.com/.well-known/openid-configuration). This JSON document lists all the endpoints (authorization, token, JWKS, userinfo, etc.) and capabilities. Your backend OIDC client library can use this URL to automatically configure itself. Authelia also provides endpoints like /jwks.json for keys, /api/oidc/authorization for auth requests, /api/oidc/token for token exchange, and /api/oidc/userinfo for additional user info (OpenID Connect 1.0 | Integration | Authelia).

2. Configuring the Backend (OIDC Client): On the application side, your backend needs to know about the Authelia IdP. If you’re using an OIDC client library or framework (such as passport-openidconnect in Node, django-allauth in Python, etc.), you’ll typically supply the Issuer URL (Authelia’s URL) or the discovery document URL, the Client ID, and Client Secret that you configured in Authelia. The library will handle building the authorization URL, validating responses, etc., according to OIDC.

If implementing manually (as our pseudocode earlier), you’ll need to set up routes:

  • A login/initiate route that redirects to Authelia (as shown in Step 1 code).
  • A callback route that processes the code (Step 3 code).

Make sure the redirect_uri in your code exactly matches one of the redirect_uris in Authelia’s client config – otherwise Authelia will refuse the request for security. Also, store and check the state parameter as shown, to prevent CSRF attacks on the login flow (Purpose of state and nonce in OpenID Connect Code flow - Stack Overflow).

3. Frontend Integration: In a traditional web app, the frontend’s role in OIDC is minimal – usually just triggering the authentication flow and handling the final redirect. For example, you might have a “Login” button or link that simply hits the /login route on your backend (which then redirects the browser to Authelia). Once the backend sets a session cookie post-authentication, the frontend will continue as usual with that session.

If your frontend is a Single Page Application (SPA), you have a couple of options:

  • Front-channel (implicit or PKCE) flow: The SPA can directly interact with Authelia’s authorization endpoint, using OAuth2/OIDC PKCE (Proof Key for Code Exchange) since SPA can’t securely store a client secret. The SPA would get the tokens from Authelia and then communicate with your backend with those tokens. However, this requires careful handling of tokens in the browser (and CORS setup for Authelia’s endpoints).
  • Backend-coordinated flow (recommended for simplicity): Even with an SPA, you can still route the user through your backend for login (as in our code above). The SPA basically opens window.location = "/login" or uses a redirect, letting the backend do the exchange and set an HttpOnly cookie. This way the SPA doesn’t directly handle the OIDC tokens, reducing exposure.

For this guide, we assume the backend-driven approach. The frontend just needs to know when a user is not logged in (perhaps by an API response or missing cookie) and then redirect the browser to the backend’s login endpoint. After the full flow, the user will be back on the SPA page with a session established.

4. Example: Using Google or Azure AD instead of Authelia: The flow is essentially the same if you use a public IdP like Google or Azure Active Directory – the main differences are in configuration. For Google, you would obtain a Client ID/Secret from the Google Developer Console and use Google’s OIDC endpoints (Google’s OAuth 2.0 implementation is OIDC-compliant and OpenID Certified (How to configure SSO with OpenID Connect in the Legacy interface – TalentLMS Support - Help Center)). For Azure AD (now part of Microsoft Entra ID), you would register an app in the Azure portal, get a Client ID/Secret, and use the Azure OIDC endpoints. The backend code doesn’t change much: it still redirects to the provider’s authorization URL and exchanges codes for tokens, just with different URLs and credentials. In fact, many libraries just need the Issuer URL changed (e.g., Google’s issuer is https://accounts.google.com and Azure’s is https://login.microsoftonline.com/<tenant>/v2.0 for instance). Both Google and Azure follow the OIDC spec, so they return ID tokens that your app can verify in the same way (OpenID Connect authentication with Microsoft Entra ID - Microsoft Entra | Microsoft Learn).

In summary, setting up OIDC involves configuring both sides: Authelia (the IdP) to trust your app, and your app to trust Authelia. Once that groundwork is in place, the actual authentication flow is handled by standard HTTP redirects and token verifications as described.

Understanding JWT

A core piece of OpenID Connect is the JWT (JSON Web Token) that carries the user’s identity information. Let’s break down what a JWT is and how it’s used in our scenario:

What is a JWT? It’s a compact, URL-safe token format consisting of three parts: a header, a payload, and a signature, serialized as header.payload.signature (JSON Web Tokens (JWTs)). For example, an ID Token might look like eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2huZG9lIiwiYXVkIjoibXktYXBwLWNsaWVudC... (header, payload, and signature separated by .).

  • The Header typically contains metadata about the token, like the signing algorithm and type. For instance: { "alg": "RS256", "typ": "JWT", "kid": "authelia-signing-key" } which indicates this token is a JWT signed with RSA256 and gives a key ID (JSON Web Tokens (JWTs)).
  • The Payload contains claims – pieces of information about the user or the token. In an OIDC ID Token, standard claims include the user’s identifier (sub – subject), the token’s audience (aud – e.g., your client ID), the issuer (iss – Authelia’s URL), expiration time (exp), issue time (iat), and possibly user profile info like name, email, or groups if those scopes were requested (How OpenID Connect Works - OpenID Foundation). It’s basically a JSON object. For example:
    {
      "sub": "johndoe",
      "aud": "my-app-client-id",
      "iss": "https://auth.example.com/",
      "exp": 1675239022,
      "iat": 1675235922,
      "email": "[email protected]",
      "name": "John Doe"
    }
    
  • The Signature is the result of taking the header and payload, and signing them using the issuer’s private key (or secret) (JSON Web Tokens (JWTs)). In our case, Authelia uses its RSA private key to sign, producing a signature that can be verified with Authelia’s public key. This cryptographic signature ensures the token hasn’t been tampered with – if anyone modifies the header or payload, the signature check will fail.

When Authelia sends our backend the id_token, it is essentially saying “Here’s a claim that user X is authenticated, issued by Authelia at time T, intended for your-app.” Our backend should verify this token:

  1. Verify the signature: Using Authelia’s public key (available via the JWKS URL (OpenID Connect 1.0 | Integration | Authelia)), we check that the signature is valid. If the token was forged or altered, this check fails. Only Authelia (with its private key) could have produced a valid signature (JSON Web Tokens (JWTs)).
  2. Verify claims: Check that iss matches Authelia’s expected issuer URL, aud matches our client ID (to ensure the token is meant for us) (Purpose of state and nonce in OpenID Connect Code flow - Stack Overflow), and that exp (expiry) and nbf/iat (if present) indicate the token is currently valid. Also, if a nonce claim is present (used in implicit/hybrid flows), it should match a value we sent to prevent token replay (Purpose of state and nonce in OpenID Connect Code flow - Stack Overflow).
  3. Trust the content: Once verified, we can trust the claims inside. For example, the sub might be the username or user ID. We might use that to lookup the user in our database or assign application roles. OIDC tokens might also include an email claim (and an email_verified flag in some IdPs) – these tell us the user’s email and whether the IdP verified it. (Be cautious: only use claims that your IdP guarantees as authoritative; see Security Considerations below about trusting claims.)

Most languages have JWT libraries that make verification straightforward. For instance, in Node using the jsonwebtoken library and a JWKS fetcher, it might look like:

const jwks = await fetch(`${AUTHELIA_BASE_URL}/jwks.json`).then((res) =>
  res.json()
);
const key = jwks.keys[0]; // in practice, find the one matching the "kid"
const publicKey = CERTfrom(key); // construct a public key object from JWKS entry
jwt.verify(
  id_token,
  publicKey,
  { algorithms: ['RS256'], audience: CLIENT_ID, issuer: AUTHELIA_ISSUER },
  (err, decoded) => {
    if (err) throw err;
    console.log('Token valid. User is:', decoded.sub);
  }
);

Many OIDC client libraries handle this under the hood for you (they won’t consider the login complete unless the ID token passes verification).

JWT for Sessions: Once our backend has a valid ID token with the user’s identity, we have a few options. One common approach is to start a traditional session (e.g., server-side session store and cookie) using the user info – essentially treating the OIDC login as a fancy “single sign-on” mechanism and then using your own session management. Another approach is to use the ID token or access token directly on subsequent requests. For example, your frontend could store the ID token in memory and include it in an Authorization header on future API calls. However, since ID tokens are meant for identity and usually short-lived, a more OAuth2-style approach is to use the Access Token for API calls and perhaps keep the ID token only for initial validation. Authelia’s access tokens by default might be opaque (not JWT) – e.g., you might see an access token like authelia_at_xyz... which would require introspection to validate (Validating Access Token #8601 - GitHub) – but Authelia can be configured to issue JWT access tokens as well. Regardless, any token used on API calls should be verified by the resource server.

For simplicity, many implementers use the ID token or a subset of its claims to create an application session:

req.session.user = {
  username: decoded.sub,
  name: decoded.name,
  email: decoded.email,
};

This ties the user’s identity to their session. From then on, req.session.user indicates an authenticated user. Just be mindful of token expiry – if the ID token expires after, say, 1 hour (as per Authelia’s lifespans.id_token setting (OpenID Connect 1.0 Provider | Configuration | Authelia)), you should consider if and when to re-authenticate or use the refresh token to get a new one. Often, refresh tokens are used to silently renew sessions without forcing the user to log in again.

Token Storage on Frontend: If you choose a token-based approach (for example, if building a stateless SPA), you might store the ID token or access token in the browser. Important: Never store tokens in plain JavaScript-accessible storage (like localStorage) without considering the security implications. A better approach is to store tokens in an HttpOnly cookie (so JS can’t read it, mitigating XSS risk), or keep them only in memory. If using cookies, set the Secure and SameSite flags to restrict their scope. We’ll cover more on these practices next.

In summary, JWTs are the vehicle through which OIDC conveys the user’s identity. The backend must verify the JWT’s signature and claims to ensure it is authentic and intended for your application (JSON Web Tokens (JWTs)). Once that’s done, the contained user information can be trusted to start a session or authorize actions.

Security Considerations

Implementing authentication and SSO flows comes with several security considerations. OIDC (and the underlying OAuth2) were designed with these in mind, but it’s critical to follow best practices. Here are key attack vectors and how to mitigate them:

  • Authorization Code Interception: In a basic OAuth2 flow without PKCE, an attacker on the network or in a malicious app could potentially intercept the authorization code and use it to obtain tokens (if they also manage to spoof or control the redirect). To mitigate this, OIDC clients should use PKCE (Proof Key for Code Exchange) – especially public clients (SPAs, mobile apps) that can’t secure a client secret. PKCE adds a hashed one-time code challenge in the auth request that must be matched by the token request (Single Sign-On implementation: Security Issues and Best Practices | SlashID Blog). In our Authelia config example, enforce_pkce: 'public_clients_only' ensures PKCE is required for non-confidential clients (OpenID Connect 1.0 Provider | Configuration | Authelia). If your client is a single-page app, always enable PKCE in the flow (most OIDC libraries do this by default now). Our earlier example did not show PKCE for brevity (assuming a confidential client), but adding it involves sending a code_challenge in step 1 and a code_verifier in step 3.

  • CSRF Attacks on the Authentication Flow: This is where an attacker tricks a logged-in user’s browser to follow an authorization redirect that returns to the attacker’s callback instead of the legitimate one. The primary defense here is the state parameter in OIDC. We saw that the backend generates a random state and validates it upon return. This binds the request and response, ensuring we’re processing the response we expected (Purpose of state and nonce in OpenID Connect Code flow - Stack Overflow). Always use a cryptographically secure random state and verify it. Additionally, if doing front-channel implicit flow with ID tokens, use the nonce parameter – it’s echoed in the ID token and helps prevent replay of tokens by tying them to the original request (Purpose of state and nonce in OpenID Connect Code flow - Stack Overflow).

  • Token Theft via XSS or Storage Vulnerabilities: If tokens (ID or access tokens) are accessible in the browser (e.g., stored in localStorage or as a window variable), a Cross-Site Scripting attack could steal them and allow an attacker to impersonate the user until the token expires. Mitigations:

    • HttpOnly Cookies: If you use cookies for session or token storage, mark them HttpOnly so scripts can’t read them, and Secure so they only travel over HTTPS. This way, even if an XSS is present, it can’t directly steal the cookie value.
    • SameSite on cookies can help prevent malicious cross-site requests from automatically including your session cookie, adding CSRF protection at the cookie level.
    • If storing tokens in the browser for an SPA, prefer in-memory storage (which vanishes on page reload) over localStorage. Or use the browser’s built-in OIDC/OAuth capabilities (for example, the Authorization Code Flow with PKCE in a popup or redirect that ultimately stores tokens in memory or cookies).
    • Use a Content Security Policy (CSP) to reduce the risk of XSS in the first place.
  • Man-in-the-Middle and Clear-text Traffic: Always use HTTPS for all OIDC interactions – the redirect to Authelia, the token endpoint call, the userinfo calls – everything. Authelia should be hosted on HTTPS. Without HTTPS, an attacker could sniff the authorization code or tokens in transit. TLS is a must to maintain the confidentiality and integrity of the flow.

  • Short Token Lifetimes and Refresh Tokens: JWTs usually have expiration (exp). Authelia’s defaults might be around 1 hour for ID token and a bit longer for refresh token (OpenID Connect 1.0 Provider | Configuration | Authelia). This limits the window if a token is stolen or compromised. It’s a good practice to keep token lifetimes short and use refresh tokens to extend sessions. However, refresh tokens themselves are sensitive – treat them like passwords. If a refresh token is stored on the client (in a SPA), it should be in a secure cookie or other secure storage. On the server side, you might store it encrypted in a database against the user’s session. Consider implementing refresh token rotation (where each use of a refresh token invalidates the old one, issuing a new one) to prevent replay if one is leaked.

  • Logout and Token Revocation: OIDC doesn’t have a single standard for logout across all IdPs, but Authelia provides a revocation endpoint (OpenID Connect 1.0 | Integration | Authelia). Be aware that if a user logs out of your app, their Authelia session might still be active (so if they come back to log in, Authelia might not prompt password again unless you configure otherwise). Authelia’s default_redirection_url can be set for where to send users after they log out from Authelia itself. Implementing logout in an SSO scenario often means logging out from both the app and the IdP. Ensure that your logout calls revoke any refresh tokens or sessions on the IdP if possible.

  • Trusting Claims and User Info: As highlighted in an OAuth/OIDC security best practices discussion, only use claims that you consider authoritative (Single Sign-On implementation: Security Issues and Best Practices | SlashID Blog) (Single Sign-On implementation: Security Issues and Best Practices | SlashID Blog). For example, the sub (subject) claim is usually a stable identifier controlled by the IdP – you can trust that to identify the user. But something like an email claim might not always be verified unless there’s an accompanying email_verified=true. In some cases, misconfigurations have allowed attackers to assert someone else’s email in a token (Single Sign-On implementation: Security Issues and Best Practices | SlashID Blog). Authelia’s IdP will only include data from its own user store, so this is less of an issue in a self-hosted scenario. But if integrating with social IdPs, always check for fields like email_verified (Google issues that) and don’t assume an email in a token is proof that the user owns that email unless verified. In short, treat the IdP as the source of truth and don’t let tokens from one IdP masquerade as another. This includes checking the iss claim – if a token’s issuer doesn’t match Authelia (or Google, etc., whichever you expect), reject it.

  • Avoiding Implicit Flow: The older implicit flow (where the IdP directly returns an id_token in the redirect URI fragment) is generally discouraged now (Single Sign-On implementation: Security Issues and Best Practices | SlashID Blog). It was used for pure client-side apps to avoid needing a token request, but it has no way to securely validate the token (no back-channel) or refresh it. The authorization code flow with PKCE is preferred even for SPAs. All major IdPs including Authelia support the code flow with PKCE for public clients. Implicit flow also tended to require putting tokens in the URL, which could end up in browser history or logs. Thus, avoid response_type=id_token (implicit) – stick to response_type=code.

  • Use of Libraries and Standards Compliance: Don’t reinvent the wheel if you can avoid it. There are many well-tested libraries for OIDC in different languages that handle the nuances of validation, clock skew, token parsing, etc. Authelia is OpenID compliant (in beta, but aiming for spec compliance) (OpenID Connect 1.0 Provider | Configuration | Authelia), and Google’s and Azure’s implementations are certified (How to configure SSO with OpenID Connect in the Legacy interface – TalentLMS Support - Help Center). Using standardized libraries will make it easier to integrate with any provider. Just ensure you configure them correctly (especially the correct redirect URI and allowed response types).



Buy Me a Coffee