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
| Term | Meaning |
|---|---|
| IdP (Identity Provider) | The identity provider: Google, Microsoft Entra ID. |
| RP / Client | Your application (Relying Party), which "trusts" the IdP. |
| ID token | A signed JWT that proves who the user is (authentication). The SSO centerpiece. |
| Access token | An authorization token to call an API. Not for authentication. |
| Refresh token | Token to get new tokens without re-authentication. |
| Scopes | Requested scope. For SSO: openid email profile. |
| Claims | User info inside the ID token (sub, email, name, email_verified…). |
| redirect_uri | URL where the IdP sends the user back. Pre-registered exactly. |
| state | Random anti-CSRF value, verified on return. |
| nonce | Random anti-replay value, bound to the ID token. |
| PKCE | Authorization code protection (RFC 7636). Required for public clients, recommended for all. |
| Discovery document | Standard /.well-known/openid-configuration URL listing all IdP endpoints. |
| JWKS | The 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.
4. The recommended flow: Authorization Code + PKCE (+ OIDC)
Standard and safe flow for web SSO. Six moments:
User click
The user clicks "Sign in with Google" or "with Microsoft".
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.
Authentication at the IdP
The user authenticates at the IdP and consents to the requested scopes.
Return with a code
The IdP redirects the browser back to your redirect_uri with a one-time code + the state.
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).
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):
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
- Go to Google Cloud Console → create/select a project.
- APIs & Services → OAuth consent screen: configure the consent screen (internal if Workspace, or external), name, support email, authorized domains.
- APIs & Services → Credentials → Create credentials → OAuth client ID: type Web Application.
- Fill in Authorized redirect URIs (e.g.
https://your-app.example.com/auth/callback) — exact, HTTPS. - Get the Client ID and Client secret.
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)
- Go to the Microsoft Entra admin center → App registrations → New registration.
- Supported account types: pick based on need — Single tenant (your org only), Multitenant (all Microsoft orgs), Multitenant + personal accounts (+ consumer Microsoft accounts).
- Authentication → Redirect URIs: add your URI (platform Web), HTTPS, exactly.
- 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.
- Get the Application (client) ID from the Overview page.
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
Initiate
Generate state, nonce, PKCE; store temporarily (signed cookie / server); redirect to the authorization endpoint.
Callback
Verify the state matches; exchange the code for tokens at the token endpoint (with code_verifier, and the secret if confidential).
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.
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.
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+SameSitecookies, not inlocalStorage. 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 profileis 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.
10. Recommended libraries (by stack)
| Stack | Library |
|---|---|
| Node.js | openid-client (OIDC-certified), or Passport with Google/Microsoft strategies |
| .NET / ASP.NET Core | Microsoft.Identity.Web, OpenIdConnect middleware |
| Java / Spring | Spring Security — OAuth2 Client |
| Python | Authlib (or python-social-auth) |
| PHP | league/oauth2-client + Google/Microsoft providers |
| Brokers / gateways | Keycloak, 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_uriregistered (HTTPS, exact) - Secrets in vault, rotation planned, deadlines tracked (Microsoft)
- PKCE +
state+nonceenabled - 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/SameSitecookies, 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
// 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
# 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
Issuer : https://accounts.google.com
Discovery : https://accounts.google.com/.well-known/openid-configuration
Microsoft Entra ID
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).