JWT is a token format, containing three base64url encoded segments separated by period (.
) character.
-
The first segment represents the JOSE Header
{"kid":"1e9gdk7","alg":"RS256"}
kid
is the key identifier, used to find a key in the JWK Set to verify the signature;alg
is the signature algorithm,RS256
here represents RSASSA-PKCS-v1_5 using SHA-256;
-
The second segment represents the claims in the token
{ "iss": "http://server.example.com", "sub": "248289761001", "aud": "s6BhdRkqt3", "nonce": "n-0S6_WzA2Mj", "exp": 1311281970, "iat": 1311280970, "name": "Jane Doe", ... }
-
The third segment represents the signature, created by signing the header and body using the specified algorithm
A RSA key pair in JWK format looks like:
{
"e": "AQAB",
"n": "3NFfKkp-P8PxG4wPN6DjB21TL2cEr7XjrZ_pzOiDUBe8SN...",
"d": "XrS9qBnDA_45zqLHdAhg1rKg1tfMPsA4IiNP1z5x80v6yR...",
"p": "_27qdcIybk4wJDsAVl1x7o10JWEc0-ha1sT5vpryOSXBcC...",
"q": "3U7Lanpkcp2vyW50ZKfssoaEAKM6VyTL3xeEWm95ZRxo9P...",
"dp": "sxaUAj5G13mwXSaU5PidUdERdsewy44kamIubAn8_D5Rc...",
"dq": "UVn0ppiFMijLBLXIrXOZK-sMvRtDh-Mr2j9P1NqjekqeP...",
"qi": "rpt-JQabYEQR5jFyix7LXiq5-LczWaxJEfyBCh17XcSKZ...",
"kty": "RSA",
"kid":"1e9gdk7",
"use": "sig"
}
ONLY the public part should be published by an OAuth server for encoding/verification, usually as a JWK Set:
{
"keys": [
{
"e": "AQAB",
"n": "xwQ72P9z9OYshiQ-n ...",
"kty": "RSA",
"kid": "r1LkbBo3925Rb2ZFFrKyU3MVex9T2817Kx0vbi6i_Kc",
"use": "sig"
},
{
"e": "AQAB",
"n": "mXauIvyeUFA74P2vcmg ...",
"kty": "RSA",
"kid": "w5kPRdJWODnYjihMgqs0tHkKk-e5OxU4DnSCZDkF_h0",
"use": "enc"
}
]
}
Refs:
- An OAuth 2.0 introduction for beginners
- Okta - OAuth 2.0 and OpenID Connect (in plain English)
- RFC 6749 - The OAuth 2.0 Authorization Framework
- OpenID Connect Core 1.0
The simple way for user authentication is using cookies:
If website A wants to access your info stored in website B, it's used to be done in this way:
Then comes OAuth2, if Yelp wants to access your Gmail account, it needs to register itself in Google as an app:
- Specify scopes, redirection url etc;
- Get a client_id and a client_secret ;
Its goal is to obtain an access_token to access protected resources
-
OAuth2.0 does not specify the format of access_token, it's up to the implementation, it can be JWT, or something else;
-
There are four flows:
- Suitable for web applications with server backend;
- The most secure, complete and complex flow;
- Usually the auth server returns a refresh_token along with the access_token;
-
PKCE: Proof Key for Code Exchange, an extension to the Authorization Code Flow;
-
Created because native apps can't safely use a client secret, so it uses a dynamic secret generated on the client;
-
Protects against authorization code stolen in transit;
-
Doesn't solve the problem of storing access token and refresh token in the front-end, you still need good a Content Security Policy and be aware of any third-party libraries you are using;
-
See:
- OAuth 2.0 Playground
- Is the OAuth 2.0 Implicit Flow Dead? | Okta Developer
- aaronpk/pkce-vanilla-js: A demonstration of the OAuth PKCE flow in plain JavaScript
-
Steps:
Generates Code Verifier and Code Challenge
// dummy code const code_verifier = getRandomString(); const code_challenge = base64url(sha256(code_verifier));
Redirect to auth server, send
code_challenge
:https://authorization-server.com/authorize? response_type=code &client_id=DQbPDZTDvFUy3G2C3z9Wp5tx &redirect_uri=https://www.oauth.com/playground/authorization-code-with-pkce.html &scope=photo+offline_access &state=zPw70UxYF_dLjn-D &code_challenge=a3jxgYEML02lNWk-OQB9aTLPPon5CBoAEdEE6eO12FM &code_challenge_method=S256
Redirect back
?state=zPw70UxYF_dLjn-D&code=IbaAu5FBoJOkK5aeSvOA-wYhcbAN9jjDeEitijeRsUJyEt9V
Exchange the code for token, use
code_verifier
here:POST https://authorization-server.com/token grant_type=authorization_code &client_id=DQbPDZTDvFUy3G2C3z9Wp5tx &redirect_uri=https://www.oauth.com/playground/authorization-code-with-pkce.html &code=IbaAu5FBoJOkK5aeSvOA-wYhcbAN9jjDeEitijeRsUJyEt9V &code_verifier=47LQewk2qOLVvz0AqV5kt_BBQB60WlImDdDj0AFrsLPRGu9n
Get back result
{ "token_type": "Bearer", "expires_in": 86400, "access_token": "4M2T7HkVKv-M5ouVL2HbBoTxtE7UuSdnJbWRqSRvv3Aji8cPjjtiFQiRNv5jefsY-vwtDPbS", "scope": "photo offline_access", "refresh_token": "jDBLg1iTwLG24380c9K5pzq3" }
- Suitable for SPA, static Javascript applications;
- The Authorization server returns the access_token directly;
- Take care to store the access_token properly;
- There is no client_secret;
- Doesn't return a refresh token (seen as too insecure), recommends short lifetime and limited scope;
- NOT SECURE, DEPRECATED NOW
- It was a compromise, created due to browser limitations (JS couldn't do cross-origin post, which is required in the auth code flow)
- Browsers, browser addons, third party JS libraries can get the access token in the url
- see: implicit-flow-dead
- Current Best Practice is to replace this with Auth Code Flow with PKCE;
- Suitable for microservices and APIS;
- No user interaction;
- Happens only on a server;
- Users enter their username and password in the client application;
- The application must be fully trusted;
- Mostly just for compatible with old systems;
This is where confusion comes from: OAuth2 was intended for authorization, but people started to use it for authentication as well, to standardize this, OpenID Connect came up
OpenID Connect is a simple identity layer on top of OAuth2, the overall data flow is the same:
A few differences:
-
The scope must contain
openid
; -
You always get an id_token in JWT format, access_token may or may not be a JWT;
-
You can get more info about the user from a userinfo endpoint;
-
Web application with server backend: Authorization code flow
- Cookie is still used to keep the session;
- access_token and id_token are stored in the session on server;
- More secure:
- Tokens are saved on the server;
- Cookies can be limited to HTTP request only, so not exposed to rogue Javascript code;
-
Native mobile app: authorization code flow with PKCE
- There is a package 'AppAuth' to handle most of the work;
-
Javascript app (SPA): authorization code flow with PKCE
- Implicit flow used to be recommended, but it's deprecated now;
- Tokens need to be stored properly, but actually there's no 100% secure way to store tokens, they are accessible by JS code, browser addons, etc;
-
Microservices and APIs: client credentials flow
A mature identify federation solution.
Three parties involved:
- User
- Identity Provider (IdP) (eg. Azure AD)
- Service Provider (SP) (eg. Salesforce)
Setup process:
- Register SP in IdP, providing ID and a bunch of URLs
- Upload IdP's certificate to SP, so SP can trust IdP
Authentication process:
- User tries to sign in
- SP identifies this is from a paticular IdP
- SP redirects user to IdP login page with a request (an encoded XML passed via GET parameter)
- IdP authenticate user and POST to SP a response (a signed XML which includes the user's attributes)
- SP validate the response using IdP certificate
- SP signs in the user
The request and response are
- In XML format
- Passed through the browser, IdP and SP do not need to talk to each other directly
Can be used as a secure alternative for password, or a method in MFA.
- Parties
- Relying party (eg. a website)
- Client (eg. a browser)
- Authenticator (eg. TPM module in a laptop, USB key, a mobile device)
- User
- Registering
- Authenticator generates a public-private key pair
- Relying party saves the public key, associate it with an account
- Authenticator keeps the private key
- Authentication
- Relying party sends a nonce (no more than once) to the client
- Client forwards it to the authenticator
- Authenticator prompts the user
- The user needs to perform some gestures to approve the intent
- Such as: some biometric proof (eg. fingerprint, facial) or a PIN
- The gesture is always performed on the authenticator device
- The nonce gets signed by the private key, and sent back
- The replying party validate the signature using the public key
WebAuthn
- JavaScript APIs for webpage to interact with authenticators in the OS
- Checks the relying party ID (eg.
example.com
) is a subset of the website URL
Passkey
- There is one unique passkey for a unique combination of
- Relying party
- User Account
- Authenticator
- Passkey could be
- Device bound (might be used in a corporate context)
- Synced (synced to cloud, more suitable for consumers)
Authenticator
- Each authenticator has a unique AAGUID (Microsoft Authenticator has different GUID for iOS and Android)
- Two types:
- Platform: built-into a user's device, eg. Windows Hello, TouchID, FaceID, smartphone's authenticator
- Roaming: removable device, eg. USB, NFC, Bluetooth
Solutions:
-
Windows Hello
- Windows 10 and up
- No sync or backup
-
Google/Android
- Android 9 and up, screen-lock turned on
- Always saved and synced with Google Password Manager
- No option to save in Microsoft Authenticator
- When on a computer, using Chrome browser, logged in to a Google account
- It will prompt you to use either the passkey on the computer
- Or use your Android device
-
Apple/iOS
- iOS/iPadOS 16 and up
- You can use the builtin "Camera" app to scan a QR code
- Could be saved in
- iCloud Keychain (synced among your Apple devices)
- or Microsoft Authenticator app (not synced)
Notes:
- For your personal Microsoft account, in your account security settings, you can add passkey as a sign-in method
- You can have one on Android, another on Apple
- Seems you can't use Windows Hello alone, you'll need a security key USB disk
- On your computer, with Google Chrome, you can create passkeys on your mobile devices, seems don't work with Firefox
- For accounts in your Entra tenant, you need to enable passkey as a method in "Authentication methods -> Policies" for your tenant first, then a user can go to his "Account -> Security" page to add a passkey
- Worked with iOS device
- Created for Android, but failed to add it to the sign-in method list