Oauth2
Oauth2¶
- Used for Authorization
- Enables third party application access to a limited version of the service
- "Login with Facebook"
Why is OAuth still hard in 2023?
Grant types
- Client Credentials: - Use if the Application is the Resource Owner
- Authorization Code: - If the application is a web app that is executing on the same server. Usually returns a Session token
- Resource Owner Password - If the Application can be trusted with the users credentials.
- Implicit: Used for Single Page Applications
- PKCE (Proof Key for Code Exchange): Used for native Application. Similar to Authorization Code
- OpenID Connect:
- Device Code:
- Hybrid:
Roles:
- Resource Owner: the entity that can grant access to a protected resource. Typically this is the end-user.
- Application: an application requesting access to a protected resource on behalf of the Resource Owner.
- Resource Server: the server hosting the protected resources. This is the API you want to access.
- Authorization Server: the server that authenticates the Resource Owner, and issues Access Tokens after getting proper authorization. In this case, Auth0.
- User Agent: the agent used by the Resource Owner to interact with the Application, for example a browser or a native application.
Authorization Code¶
- Get a token for your email app to access your google mail.
Note
The token gets sent from google directly to the application server. Does not go through the browser
Protocol Flow:
sequenceDiagram
Application (Client) ->> User (Resource Owner): 1. Authorization Request
User (Resource Owner) ->> Application (Client): 2. Authorization Grant
Application (Client) ->> Authorization Server: 3. Authorization Grant
Authorization Server ->> Application (Client): 4. Access Token
Application (Client) ->> Resource Server: 5. Access Token
Resource Server ->> Application (Client): 6. Protected Resource
Parameters¶
https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml
Name | Parameter Usage Location |
---|---|
client_id | authorization request, token request |
client_secret | token request |
response_type | authorization request |
redirect_uri | authorization request, token request |
scope | authorization request, authorization response, token request, token response |
state | authorization request, authorization response |
code | authorization response, token request |
error | authorization response, token response |
error_description | authorization response, token response |
error_uri | authorization response, token response |
grant_type | token request |
access_token | authorization response, token response |
token_type | authorization response, token response |
expires_in | authorization response, token response |
username | token request |
password | token request |
refresh_token | token request, token response |
nonce | authorization request |
display | authorization request |
prompt | authorization request |
max_age | authorization request |
ui_locales | authorization request |
claims_locales | authorization request |
id_token_hint | authorization request |
login_hint | authorization request |
acr_values | authorization request |
claims | authorization request |
registration | authorization request |
request | authorization request |
request_uri | authorization request |
id_token | authorization response, access token response |
session_state | authorization response, access token response |
assertion | token request |
client_assertion | token request |
client_assertion_type | token request |
code_verifier | token request |
code_challenge | authorization request |
code_challenge_method | authorization request |
claim_token | client request, token endpoint |
pct | client request, token endpoint |
pct | authorization server response, token endpoint |
rpt | client request, token endpoint |
ticket | client request, token endpoint |
upgraded | authorization server response, token endpoint |
vtr | authorization request, token request |
device_code | token request |
resource | authorization request, token request |
audience | token request |
requested_token_type | token request |
subject_token | token request |
subject_token_type | token request |
actor_token | token request |
actor_token_type | token request |
issued_token_type | token response |
response_mode | Authorization Request |
nfv_token | Access Token Response |
iss | authorization request, authorization response |
sub | authorization request |
aud | authorization request |
exp | authorization request |
nbf | authorization request |
iat | authorization request |
jti | authorization request |
ace_profile | token response |
nonce1 | client-rs request |
nonce2 | rs-client response |
ace_client_recipientid | client-rs request |
ace_server_recipientid | rs-client response |
req_cnf | token request |
rs_cnf | token response |
cnf | token response |
authorization_details | authorization request, token request, token response |
dpop_jkt | authorization request |
Attacks¶
Reusing Account tokens¶
Check that an old Code is expired after one use. Or after a short time period.
Insufficient redirect URI validation¶
Check the redirect URI for an attacker controlled site.
Check for a 302 Redirect on the scoped domain
CSRF¶
Check that State parameter
- Make sure that it is checked by the server
- Make sure that it is not a static value
- Make sure that if you remove the parameter all together that the request fails.
Otherwise it can be subject to CSRF attacks
Google OAUTH Example¶
Setup:
1. Go to https://console.cloud.google.com/apis/credentials and set up a new OAuth 2.0 Client IDs
2. Create Credentials -> OAuth client ID -> Web Application
3. Set Authorized JavaScript origins to http://test.generalzero.org:5000/
4. Set Authorized redirect URIs to http://test.generalzero.org:5000/oauth/google/callback
5. Set the client_id and client secret to env variables.
Flow:
1. On visit to http://test.generalzero.org:5000/
there is a link to /oauth/google
2. When the user visits /oauth/google
the site returns a 302 redirect to Googles OAUTH endpoint with some parameters.
- Google Parameters {"response_type": "code", "client_id":"{OAuth_Client_ID}", "redirect_uri": "http%3A%2F%test.generalzero.org%3A5000%2Foauth%2Fgoogle%2Fcallback", "scope": "email", "state":"fb139654c956ac64dfb39e0000000000"}
- The site also sets a oauth_state cookie with the test.generalzero.org domain oauth_state=fb139654c956ac64dfb39e0000000000; HttpOnly; SameSite=Lax
3. Google Does its flow and then makes a 302 redirect back to the redirect_uri.
- Parameters: {"state":"fb139654c956ac64dfb39e0000000000", "code":"4%2F0AcvDMrDA-4AAAAAAAAAA8ffzMy4XJ0qSDFEt_1SfYAAAAxSl7YBDfFSdtALVsQL4JmYfdQ", "scope":"email+openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email", "authuser":"0", "prompt":"none"
4. On the /oauth/google/callback
the endpoint checks that the state paramater is the same as the set oauth_state cookie that is submitted on the request from the browser.
5. This means that you have a valid token and code to query google as the server.
6. THE SERVER makes a request to google to retrieve an access token.
- Parameters: {"grant_type":"authorization_code", "code":"4%2F0AcvDMrDA-4AAAAAAAAAA8ffzMy4XJ0qSDFEt_1SfYAAAAxSl7YBDfFSdtALVsQL4JmYfdQ", "redirect_uri":"http%3A%2F%test.generalzero.org%3A5000%2Foauth%2Fgoogle%2Fcallback", "client_id":"{OAuth_Client_ID}", "client_secret":"{OAuth_Client_SECRET}"}
7. Now the server has the access_token from google to request information about the user.
- https://www.googleapis.com/oauth2/v2/userinfo with the Header Authorization: Bearer ${access_token}
Test Code:
import express from "express";
import crypto from "crypto";
const PORT = 5000;
const CLIENT_ID = process.env.CLIENT_ID as string;
const CLIENT_SECRET = process.env.CLIENT_SECRET as string;
const DOMAIN = "http://test.generalzero.org"
const SECURE = false;
const app = express();
app.use(express.static("static"));
app.get("/oauth/google", (req, res) => {
const params = new URLSearchParams();
params.set("response_type", "code");
params.set("client_id", CLIENT_ID);
params.set("redirect_uri", `${DOMAIN}:${PORT}/oauth/google/callback`);
params.set("scope", "email");
const state = crypto.randomBytes(16).toString("hex");
params.set("state", state);
const url = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
if (SECURE){
res.set("Set-Cookie", `oauth_state=${state}; HttpOnly; Secure; SameSite=Lax`);
}
else {
res.set("Set-Cookie", `oauth_state=${state}; HttpOnly; SameSite=Lax`);
}
res.redirect(url);
});
app.get("/oauth/google/callback", async (req, res) => {
const { code, state } = req.query;
const oauthState = getCookie("oauth_state", req.headers.cookie as string);
console.log("/oauth/google/callback")
console.log(`req.headers.cookie: ${req.headers.cookie}`)
console.log(`req.query: ${req.query}`)
if (state !== oauthState) {
return res.status(400).send("Invalid state");
}
const accessToken = await exchangeCodeForToken(code as string);
const userInfo = await getUserInfo(accessToken);
res.header("Content-Type", "application/json").send(JSON.stringify(userInfo));
});
app.listen(PORT, () => {
console.log(`Example app listening at ${DOMAIN}:${PORT}`);
});
function getCookie(name: string, cookies: string) {
console.log(`name:${name}, cookie: ${cookies} `)
const cookie = cookies.split(";").find((cookie) => cookie.trim().startsWith(`${name}=`));
if (!cookie) {
return null;
}
return cookie.split("=")[1];
}
async function exchangeCodeForToken(code: string) {
const resp = await fetch(`https://oauth2.googleapis.com/token`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: `${DOMAIN}:${PORT}/oauth/google/callback`,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
}).toString(),
});
if (!resp.ok) {
throw new Error("Something went wrong");
}
const { access_token } = (await resp.json()) as { access_token: string };
return access_token;
}
async function getUserInfo(accessToken: string) {
const resp = await fetch(`https://www.googleapis.com/oauth2/v2/userinfo`, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const json = (await resp.json()) as { email: string };
return json;
}