Skip to main content
Mag&Cie
Back to tutorials
FreeIntermediateCybersecurity

Add Google and Microsoft SSO to your app — Complete OIDC guide

Complete vendor-neutral guide to add 'Sign in with Google' and 'Sign in with Microsoft' to your application. OpenID Connect (OIDC), Authorization Code + PKCE flow, Google Cloud and Entra ID setup, ID token validation, production checklist, Node.js and Python examples.

June 26, 202645 min

What you'll learn

  • Understand the OIDC mechanics without reinventing cryptography
  • Properly configure a Google OAuth client + a Microsoft Entra App Registration
  • Implement the Authorization Code + PKCE flow with a certified library
  • Validate the ID token server-side (signature, iss, aud, exp, nonce)
  • Avoid the 6 pitfalls that turn an SSO into a security flaw

Prerequisites

  • Web or mobile app where you control the backend (or an identity broker)
  • Familiarity with HTTPS, JSON Web Tokens (JWT), environment variables
  • Admin access to Google Cloud Console and/or Microsoft Entra admin center
On this page18

1. Goal & scope

Let a user sign in to your app with their Google (Gmail / Google Workspace) or Microsoft (work/school Entra ID account, or personal account) credentials, without creating a new password — that's SSO (Single Sign-On).

The standard to use is OpenID Connect (OIDC), an authentication layer on top of OAuth 2.0. Google and Microsoft are both compliant OIDC providers: the same mechanism applies to both, only configuration parameters change.

Guiding principle

Never rewrite cryptography or token validation yourself. Use a certified OIDC library (openid-client, Authlib, Microsoft.Identity.Web, Spring Security…) or an identity broker (Keycloak, Auth0/Okta, Microsoft Entra External ID). This guide gives you the vocabulary and the right configuration — the library does the rest.

2. Essential vocabulary

TermMeaning
IdP (Identity Provider)The identity provider: Google, Microsoft Entra ID.
RP / ClientYour application (Relying Party), which "trusts" the IdP.
ID tokenA signed JWT that proves who the user is (authentication). The SSO centerpiece.
Access tokenAn authorization token to call an API. Not for authentication.
Refresh tokenToken to get new tokens without re-authentication.
ScopesRequested scope. For SSO: openid email profile.
ClaimsUser info inside the ID token (sub, email, name, email_verified…).
redirect_uriURL where the IdP sends the user back. Pre-registered exactly.
stateRandom anti-CSRF value, verified on return.
nonceRandom anti-replay value, bound to the ID token.
PKCEAuthorization code protection (RFC 7636). Required for public clients, recommended for all.
Discovery documentStandard /.well-known/openid-configuration URL listing all IdP endpoints.
JWKSThe IdP's public key set used to verify token signatures.

3. Choosing your approach

3.1 App type (drives security)

Confidential client (server-side web)

The backend holds a secret. Uses a client_secret (or a certificate). Exchanges the code server-side. Most common case.

Public client (SPA / mobile / desktop)

No secret (it would be exposed). PKCE is mandatory. The OIDC library handles it for you.

3.2 Direct integration or broker?

One OIDC library per provider

Recommended to start simple. Google and Microsoft integrated as two OIDC providers behind a common layer in your app.

Identity broker (Keycloak, Auth0, Entra External ID…)

One integration point. The broker handles Google, Microsoft, SAML, MFA, sessions. Required once you have multiple IdPs, multiple apps, or enterprise needs (MFA, SCIM, audit).

Whichever route you pick, treat Google and Microsoft homogeneously: two "Sign in with…" buttons triggering the same OIDC flow with different parameters.

Standard and safe flow for web SSO. Six moments:

1

User click

The user clicks "Sign in with Google" or "with Microsoft".

2

Redirect to IdP

Your app generates state, nonce and a PKCE pair (code_verifier secret + code_challenge), then redirects the browser to the IdP's authorization endpoint.

3

Authentication at the IdP

The user authenticates at the IdP and consents to the requested scopes.

4

Return with a code

The IdP redirects the browser back to your redirect_uri with a one-time code + the state.

5

Code exchange

Your backend verifies the state, then exchanges the code for tokens at the token endpoint (sending the code_verifier, and the secret if confidential).

6

Validation + session

Your app validates the ID token, extracts identity, creates/links the local account, opens an app session.

Example authorization URL (key params, shared by both IdPs):

Texte
GET https://<IdP_authorize_endpoint>
  ?client_id=YOUR_CLIENT_ID
  &response_type=code
  &redirect_uri=https://your-app.example.com/auth/callback
  &scope=openid email profile
  &state=RANDOM_ANTI_CSRF
  &nonce=RANDOM_ANTI_REPLAY
  &code_challenge=BASE64URL_SHA256_OF_VERIFIER
  &code_challenge_method=S256

5. Setup on the Google side

  1. Go to Google Cloud Console → create/select a project.
  2. APIs & Services → OAuth consent screen: configure the consent screen (internal if Workspace, or external), name, support email, authorized domains.
  3. APIs & Services → Credentials → Create credentials → OAuth client ID: type Web Application.
  4. Fill in Authorized redirect URIs (e.g. https://your-app.example.com/auth/callback) — exact, HTTPS.
  5. Get the Client ID and Client secret.
Texte
Discovery : https://accounts.google.com/.well-known/openid-configuration
Authorize : https://accounts.google.com/o/oauth2/v2/auth
Token     : https://oauth2.googleapis.com/token
JWKS      : https://www.googleapis.com/oauth2/v3/certs
UserInfo  : https://openidconnect.googleapis.com/v1/userinfo
Issuer    : https://accounts.google.com

SSO scopes: openid email profile. Useful claims: sub (stable identifier), email, email_verified, name, picture, hd (Workspace domain).

6. Setup on the Microsoft side (Entra ID)

  1. Go to the Microsoft Entra admin centerApp registrationsNew registration.
  2. Supported account types: pick based on need — Single tenant (your org only), Multitenant (all Microsoft orgs), Multitenant + personal accounts (+ consumer Microsoft accounts).
  3. Authentication → Redirect URIs: add your URI (platform Web), HTTPS, exactly.
  4. Certificates & secrets: create a client secret (copy the Value, not the Secret ID; set an expiration and plan rotation). Prefer a certificate to a secret when possible.
  5. Get the Application (client) ID from the Overview page.
Texte
Discovery : https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration
Authorize : https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize
Token     : https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
JWKS      : https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys
UserInfo  : https://graph.microsoft.com/oidc/userinfo

{tenant} = common (all) / organizations (work/school) / consumers (personal) / specific tenant ID. SSO scopes: openid email profile. Useful claims: sub, oid (stable user identifier inside the tenant), tid (tenant), email / preferred_username.

⚠️ The Microsoft multi-tenant trap

The issuer contains a variable tenant identifier. If you don't explicitly validate authorized tid / iss, any Microsoft tenant will sign in to your app — an attacker just needs to spin up a free Entra tenant. Always maintain an explicit allowlist of authorized tid values server-side.

7. App-side implementation steps

1

Initiate

Generate state, nonce, PKCE; store temporarily (signed cookie / server); redirect to the authorization endpoint.

2

Callback

Verify the state matches; exchange the code for tokens at the token endpoint (with code_verifier, and the secret if confidential).

3

Validate the ID token

Server-side, mandatory: signature via the JWKS, iss = expected issuer, aud = your client_id, exp / iat valid (clock skew tolerance ~ a few minutes), nonce = the one you sent.

4

Identity & account

Read claims, link to local account by verified email (email_verified = true) or by sub / oid; create the account if needed; open the app session.

5

Logout

Invalidate the local session; optionally trigger logout at the IdP via the end_session_endpoint.

8. Non-negotiable security

All of it is your responsibility — not the provider's

SSO integration security is entirely on your application's side. The IdP does its job if you do yours correctly.

  • HTTPS everywhere. No exceptions, including staging.
  • Exact pre-registered redirect_uri: no wildcards, mind casing and trailing slash (strict match).
  • state (anti-CSRF) + nonce (anti-replay) always, and PKCE even for confidential clients.
  • Server-side ID token validation (signature, iss, aud, exp, nonce) — never trust an unverified token.
  • Secrets server-side only: never in a SPA, mobile app, or Git repo. Vault, rotation, expiration. On Microsoft side, prefer a certificate to a secret.
  • Session/token storage: HttpOnly + Secure + SameSite cookies, not in localStorage. Short sessions, refresh token rotation.
  • Account linking by verified email only (email_verified). Otherwise, account takeover risk.
  • Microsoft multi-tenant: validate authorized tid / iss.
  • Minimize scopes (openid email profile is enough; only request API scopes when actually needed).
  • JWKS: caching + handling key rotation by the IdP.
  • Logging of logins/failures, anti-bruteforce, and access revocation when an employee leaves.

9. The 6 most common pitfalls

1 — Authenticating with the access token

The access token is an authorization token, not an authentication one. The ID token is the single source of truth for SSO.

2 — redirect_uri not exact

Slash, casing, http/https: strict match. One extra character → redirect error.

3 — No tenant validation

In Microsoft multi-tenant, without a tid / iss allowlist, any Microsoft tenant signs in to your app.

4 — Secret exposed in a public client

A secret in a SPA, mobile app, or Git repo is a public secret. Use PKCE without a secret.

5 — Linking on unverified email

Without email_verified = true, anyone can create an account in someone else's name at a lax IdP and take over.

6 — Microsoft secret expired

Without scheduled rotation, expiration hits in production. Keep several secrets active in parallel = no-downtime switch.

StackLibrary
Node.jsopenid-client (OIDC-certified), or Passport with Google/Microsoft strategies
.NET / ASP.NET CoreMicrosoft.Identity.Web, OpenIdConnect middleware
Java / SpringSpring Security — OAuth2 Client
PythonAuthlib (or python-social-auth)
PHPleague/oauth2-client + Google/Microsoft providers
Brokers / gatewaysKeycloak, Authelia (open source); Auth0/Okta, Microsoft Entra External ID (managed)

11. Production checklist

Before go-live

Tick every box. Test with a guinea-pig account on each branch of the matrix (new / existing / consent denied / expired token / unverified email / wrong tenant).

  • OAuth clients created on Google and Microsoft sides, prod redirect_uri registered (HTTPS, exact)
  • Secrets in vault, rotation planned, deadlines tracked (Microsoft)
  • PKCE + state + nonce enabled
  • Full ID token validation (signature/JWKS, iss, aud, exp, nonce)
  • Microsoft tenant validated (if multi-tenant); verified email required for account linking
  • Sessions in HttpOnly / Secure / SameSite cookies, short durations, working logout
  • Minimal scopes; logging and revocation procedure in place
  • Tests: new account, existing account, consent denied, expired token, unverified email, wrong tenant

12. Annotated code examples

How to read the examples

The essentials (PKCE, state, nonce, ID token validation) are delegated to a certified library. The same code serves both Google and Microsoft — only the issuer and credentials change. Adapt to your library version.

12.1 Node.js — Express + openid-client

JavaScript
// OIDC SSO with Google and Microsoft — Node.js (Express + openid-client v5)
// npm i express express-session openid-client
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: false,
  cookie: { httpOnly: true, secure: true, sameSite: 'lax' },
}));

const REDIRECT = 'https://your-app.example.com/auth/callback';

async function makeClient(issuerUrl, clientId, clientSecret, redirectUri) {
  const issuer = await Issuer.discover(issuerUrl);
  return new issuer.Client({
    client_id: clientId,
    client_secret: clientSecret, // server-side ONLY (confidential client)
    redirect_uris: [redirectUri],
    response_types: ['code'],
  });
}

const clients = {};
(async () => {
  clients.google = await makeClient('https://accounts.google.com',
    process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET, REDIRECT + '/google');
  clients.microsoft = await makeClient('https://login.microsoftonline.com/<tenant>/v2.0',
    process.env.MS_CLIENT_ID, process.env.MS_CLIENT_SECRET, REDIRECT + '/microsoft');
})();

app.get('/login/:idp', (req, res) => {
  const client = clients[req.params.idp];
  const code_verifier = generators.codeVerifier();
  const code_challenge = generators.codeChallenge(code_verifier);
  const state = generators.state();
  const nonce = generators.nonce();
  req.session.oidc = { idp: req.params.idp, code_verifier, state, nonce };
  res.redirect(client.authorizationUrl({
    scope: 'openid email profile',
    code_challenge, code_challenge_method: 'S256', state, nonce,
  }));
});

app.get('/auth/callback/:idp', async (req, res) => {
  const client = clients[req.params.idp];
  const { code_verifier, state, nonce } = req.session.oidc || {};
  const params = client.callbackParams(req);
  const tokenSet = await client.callback(
    client.metadata.redirect_uris[0], params,
    { code_verifier, state, nonce }, // openid-client validates everything
  );
  const claims = tokenSet.claims(); // VALIDATED ID token contents

  if (!claims.email || claims.email_verified === false) {
    return res.status(403).send('Email not verified by the provider.');
  }
  const user = await findOrCreateUser({
    provider: req.params.idp, subject: claims.sub, email: claims.email, name: claims.name,
  });
  req.session.userId = user.id;
  delete req.session.oidc;
  res.redirect('/');
});

app.get('/logout', (req, res) => req.session.destroy(() => res.redirect('/')));

openid-client versions

Example in openid-client v5 API. v6 exposes a different functional API — the logic (discovery, PKCE, validation) is the same.

12.2 Python — Flask + Authlib

Python
# OIDC SSO with Google and Microsoft — Python (Flask + Authlib)
# pip install Flask Authlib
import os
from flask import Flask, session, redirect, url_for, abort
from authlib.integrations.flask_client import OAuth

app = Flask(__name__)
app.secret_key = os.environ["SECRET_KEY"]
app.config.update(SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SECURE=True,
                  SESSION_COOKIE_SAMESITE="Lax")

oauth = OAuth(app)

oauth.register(
    name="google",
    server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
    client_id=os.environ["GOOGLE_CLIENT_ID"],
    client_secret=os.environ["GOOGLE_CLIENT_SECRET"],
    client_kwargs={"scope": "openid email profile"},
)
oauth.register(
    name="microsoft",
    server_metadata_url="https://login.microsoftonline.com/<tenant>/v2.0/.well-known/openid-configuration",
    client_id=os.environ["MS_CLIENT_ID"],
    client_secret=os.environ["MS_CLIENT_SECRET"],
    client_kwargs={"scope": "openid email profile"},
)

@app.route("/login/<idp>")
def login(idp):
    client = oauth.create_client(idp) or abort(404)
    redirect_uri = url_for("callback", idp=idp, _external=True)
    return client.authorize_redirect(redirect_uri)

@app.route("/auth/callback/<idp>")
def callback(idp):
    client = oauth.create_client(idp) or abort(404)
    token = client.authorize_access_token()  # verifies state, exchanges code
    claims = token["userinfo"]  # ID token already validated

    if not claims.get("email") or claims.get("email_verified") is False:
        abort(403, "Email not verified by the provider.")
    user = find_or_create_user(provider=idp, subject=claims["sub"],
                               email=claims["email"], name=claims.get("name"))
    session["user_id"] = user.id
    return redirect("/")

@app.route("/logout")
def logout():
    session.clear()
    return redirect("/")

Endpoints recap

Google

Texte
Issuer    : https://accounts.google.com
Discovery : https://accounts.google.com/.well-known/openid-configuration

Microsoft Entra ID

Texte
Discovery : https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration
({tenant} = common | organizations | consumers | <tenant-id>)

Portal names and screens may evolve; the discovery documents above remain the source of truth for endpoints, scopes and keys for each provider.

Want a cybersecurity audit of your SSO integration?

MAG&Cie engagement

If you'd like a review of your SSO integration before going to production — Microsoft tenant validation, secret rotation, cookie configuration, logging, GDPR audit points — that's exactly the scope covered by the Cybersecurity Posture Audit or an accelerated Cyber-Audit (report within 48h).