Skip to content

Commit 95b7fcc

Browse files
authored
uberf-9797: idp auth state (#9196)
1 parent 2005c19 commit 95b7fcc

File tree

4 files changed

+86
-15
lines changed

4 files changed

+86
-15
lines changed

pods/authProviders/src/github.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import { BrandingMap, concatLink, MeasureContext, getBranding, SocialIdType } fr
44
import Router from 'koa-router'
55
import { Strategy as GitHubStrategy } from 'passport-github2'
66
import { Passport } from '.'
7-
import { encodeState, handleProviderAuth, safeParseAuthState } from './utils'
7+
import {
8+
encodeState,
9+
handleProviderAuth,
10+
safeParseAuthState,
11+
setAuthStateTokenCookie,
12+
validateAuthStateTokenCookie
13+
} from './utils'
814

915
export function registerGithub (
1016
measureCtx: MeasureContext,
@@ -39,7 +45,11 @@ export function registerGithub (
3945

4046
router.get('/auth/github', async (ctx, next) => {
4147
measureCtx.info('try auth via', { provider: 'github' })
42-
const state = encodeState(ctx, brandings)
48+
49+
const nonce = crypto.randomUUID()
50+
setAuthStateTokenCookie(ctx, nonce)
51+
52+
const state = encodeState(ctx, brandings, nonce)
4353

4454
passport.authenticate('github', { scope: ['user:email'], session: true, state })(ctx, next)
4555
})
@@ -49,9 +59,15 @@ export function registerGithub (
4959
async (ctx, next) => {
5060
const state = safeParseAuthState(ctx.query?.state)
5161
const branding = getBranding(brandings, state?.branding)
62+
const loginUrl = concatLink(branding?.front ?? frontUrl, '/login')
63+
const isValidState = validateAuthStateTokenCookie(ctx, state?.nonce)
64+
if (!isValidState) {
65+
ctx.redirect(loginUrl)
66+
return
67+
}
5268

5369
await passport.authenticate('github', {
54-
failureRedirect: concatLink(branding?.front ?? frontUrl, '/login'),
70+
failureRedirect: loginUrl,
5571
session: true
5672
})(ctx, next)
5773
},

pods/authProviders/src/google.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import { BrandingMap, concatLink, MeasureContext, getBranding, SocialIdType } fr
44
import Router from 'koa-router'
55
import { Strategy as GoogleStrategy } from 'passport-google-oauth20'
66
import { Passport } from '.'
7-
import { encodeState, handleProviderAuth, safeParseAuthState } from './utils'
7+
import {
8+
encodeState,
9+
handleProviderAuth,
10+
safeParseAuthState,
11+
setAuthStateTokenCookie,
12+
validateAuthStateTokenCookie
13+
} from './utils'
814

915
export function registerGoogle (
1016
measureCtx: MeasureContext,
@@ -39,7 +45,11 @@ export function registerGoogle (
3945

4046
router.get('/auth/google', async (ctx, next) => {
4147
measureCtx.info('try auth via', { provider: 'google' })
42-
const state = encodeState(ctx, brandings)
48+
49+
const nonce = crypto.randomUUID()
50+
setAuthStateTokenCookie(ctx, nonce)
51+
52+
const state = encodeState(ctx, brandings, nonce)
4353

4454
passport.authenticate('google', { scope: ['profile', 'email'], session: true, state })(ctx, next)
4555
})
@@ -48,13 +58,17 @@ export function registerGoogle (
4858
redirectURL,
4959
async (ctx, next) => {
5060
const state = safeParseAuthState(ctx.query?.state)
51-
measureCtx.info('Auth state', { state })
5261
const branding = getBranding(brandings, state?.branding)
53-
measureCtx.info('With branding', { branding })
54-
const failureRedirect = concatLink(branding?.front ?? frontUrl, '/login')
55-
measureCtx.info('With failure redirect', { failureRedirect })
62+
63+
const loginUrl = concatLink(branding?.front ?? frontUrl, '/login')
64+
const isValidState = validateAuthStateTokenCookie(ctx, state?.nonce)
65+
if (!isValidState) {
66+
ctx.redirect(loginUrl)
67+
return
68+
}
69+
5670
await passport.authenticate('google', {
57-
failureRedirect,
71+
failureRedirect: loginUrl,
5872
session: true
5973
})(ctx, next)
6074
},

pods/authProviders/src/openid.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ import Router from 'koa-router'
1919
import { Issuer, Strategy } from 'openid-client'
2020

2121
import { Passport } from '.'
22-
import { encodeState, handleProviderAuth, safeParseAuthState } from './utils'
22+
import {
23+
encodeState,
24+
handleProviderAuth,
25+
safeParseAuthState,
26+
setAuthStateTokenCookie,
27+
validateAuthStateTokenCookie
28+
} from './utils'
2329

2430
export function registerOpenid (
2531
measureCtx: MeasureContext,
@@ -66,7 +72,11 @@ export function registerOpenid (
6672

6773
router.get('/auth/openid', async (ctx, next) => {
6874
measureCtx.info('try auth via', { provider: 'openid' })
69-
const state = encodeState(ctx, brandings)
75+
76+
const nonce = crypto.randomUUID()
77+
setAuthStateTokenCookie(ctx, nonce)
78+
79+
const state = encodeState(ctx, brandings, nonce)
7080

7181
await passport.authenticate('oidc', {
7282
scope: 'openid profile email',
@@ -79,9 +89,15 @@ export function registerOpenid (
7989
async (ctx, next) => {
8090
const state = safeParseAuthState(ctx.query?.state)
8191
const branding = getBranding(brandings, state?.branding)
92+
const loginUrl = concatLink(branding?.front ?? frontUrl, '/login')
93+
const isValidState = validateAuthStateTokenCookie(ctx, state?.nonce)
94+
if (!isValidState) {
95+
ctx.redirect(loginUrl)
96+
return
97+
}
8298

8399
await passport.authenticate('oidc', {
84-
failureRedirect: concatLink(branding?.front ?? frontUrl, '/login')
100+
failureRedirect: loginUrl
85101
})(ctx, next)
86102
},
87103
async (ctx, next) => {

pods/authProviders/src/utils.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export interface AuthState {
3232
branding?: string
3333
autoJoin?: boolean
3434
navigateUrl?: string
35+
nonce?: string
3536
}
3637

3738
export function safeParseAuthState (rawState: string | undefined): AuthState {
@@ -46,14 +47,15 @@ export function safeParseAuthState (rawState: string | undefined): AuthState {
4647
}
4748
}
4849

49-
export function encodeState (ctx: any, brandings: BrandingMap): string {
50+
export function encodeState (ctx: any, brandings: BrandingMap, nonce: string): string {
5051
const host = getHost(ctx.request.headers)
5152
const branding = host !== undefined ? brandings[host]?.key ?? undefined : undefined
5253
const state: AuthState = {
5354
inviteId: ctx.query?.inviteId,
5455
branding,
5556
autoJoin: ctx.query?.autoJoin !== undefined,
56-
navigateUrl: ctx.query?.navigateUrl
57+
navigateUrl: ctx.query?.navigateUrl,
58+
nonce
5759
}
5860

5961
return encodeURIComponent(JSON.stringify(state))
@@ -131,3 +133,26 @@ export async function handleProviderAuth (
131133
return ''
132134
}
133135
}
136+
137+
const authStateTokenCookie = 'auth_state_token'
138+
139+
export function setAuthStateTokenCookie (ctx: any, nonce: string): void {
140+
ctx.cookies.set(authStateTokenCookie, nonce, {
141+
httpOnly: true, // Prevents JavaScript access to cookie
142+
secure: true, // Only sent over HTTPS connections
143+
sameSite: 'lax', // Prevents CSRF by controlling when cookie is sent cross-site
144+
signed: true, // Signs cookie with app.keys to prevent tampering
145+
maxAge: 10 * 60 * 1000 // 10 minutes
146+
})
147+
}
148+
149+
export function validateAuthStateTokenCookie (ctx: any, expectedNonce: string | undefined): boolean {
150+
const nonce = ctx.cookies.get(authStateTokenCookie)
151+
ctx.cookies.set(authStateTokenCookie, null)
152+
153+
if (nonce === undefined || expectedNonce === undefined || nonce !== expectedNonce) {
154+
return false
155+
}
156+
157+
return true
158+
}

0 commit comments

Comments
 (0)