uberf-9797: idp auth state (#9196)

This commit is contained in:
Alexey Zinoviev
2025-06-10 08:56:14 +07:00
committed by GitHub
parent 2005c19666
commit 95b7fccca2
4 changed files with 86 additions and 15 deletions
+19 -3
View File
@@ -4,7 +4,13 @@ import { BrandingMap, concatLink, MeasureContext, getBranding, SocialIdType } fr
import Router from 'koa-router'
import { Strategy as GitHubStrategy } from 'passport-github2'
import { Passport } from '.'
import { encodeState, handleProviderAuth, safeParseAuthState } from './utils'
import {
encodeState,
handleProviderAuth,
safeParseAuthState,
setAuthStateTokenCookie,
validateAuthStateTokenCookie
} from './utils'
export function registerGithub (
measureCtx: MeasureContext,
@@ -39,7 +45,11 @@ export function registerGithub (
router.get('/auth/github', async (ctx, next) => {
measureCtx.info('try auth via', { provider: 'github' })
const state = encodeState(ctx, brandings)
const nonce = crypto.randomUUID()
setAuthStateTokenCookie(ctx, nonce)
const state = encodeState(ctx, brandings, nonce)
passport.authenticate('github', { scope: ['user:email'], session: true, state })(ctx, next)
})
@@ -49,9 +59,15 @@ export function registerGithub (
async (ctx, next) => {
const state = safeParseAuthState(ctx.query?.state)
const branding = getBranding(brandings, state?.branding)
const loginUrl = concatLink(branding?.front ?? frontUrl, '/login')
const isValidState = validateAuthStateTokenCookie(ctx, state?.nonce)
if (!isValidState) {
ctx.redirect(loginUrl)
return
}
await passport.authenticate('github', {
failureRedirect: concatLink(branding?.front ?? frontUrl, '/login'),
failureRedirect: loginUrl,
session: true
})(ctx, next)
},
+21 -7
View File
@@ -4,7 +4,13 @@ import { BrandingMap, concatLink, MeasureContext, getBranding, SocialIdType } fr
import Router from 'koa-router'
import { Strategy as GoogleStrategy } from 'passport-google-oauth20'
import { Passport } from '.'
import { encodeState, handleProviderAuth, safeParseAuthState } from './utils'
import {
encodeState,
handleProviderAuth,
safeParseAuthState,
setAuthStateTokenCookie,
validateAuthStateTokenCookie
} from './utils'
export function registerGoogle (
measureCtx: MeasureContext,
@@ -39,7 +45,11 @@ export function registerGoogle (
router.get('/auth/google', async (ctx, next) => {
measureCtx.info('try auth via', { provider: 'google' })
const state = encodeState(ctx, brandings)
const nonce = crypto.randomUUID()
setAuthStateTokenCookie(ctx, nonce)
const state = encodeState(ctx, brandings, nonce)
passport.authenticate('google', { scope: ['profile', 'email'], session: true, state })(ctx, next)
})
@@ -48,13 +58,17 @@ export function registerGoogle (
redirectURL,
async (ctx, next) => {
const state = safeParseAuthState(ctx.query?.state)
measureCtx.info('Auth state', { state })
const branding = getBranding(brandings, state?.branding)
measureCtx.info('With branding', { branding })
const failureRedirect = concatLink(branding?.front ?? frontUrl, '/login')
measureCtx.info('With failure redirect', { failureRedirect })
const loginUrl = concatLink(branding?.front ?? frontUrl, '/login')
const isValidState = validateAuthStateTokenCookie(ctx, state?.nonce)
if (!isValidState) {
ctx.redirect(loginUrl)
return
}
await passport.authenticate('google', {
failureRedirect,
failureRedirect: loginUrl,
session: true
})(ctx, next)
},
+19 -3
View File
@@ -19,7 +19,13 @@ import Router from 'koa-router'
import { Issuer, Strategy } from 'openid-client'
import { Passport } from '.'
import { encodeState, handleProviderAuth, safeParseAuthState } from './utils'
import {
encodeState,
handleProviderAuth,
safeParseAuthState,
setAuthStateTokenCookie,
validateAuthStateTokenCookie
} from './utils'
export function registerOpenid (
measureCtx: MeasureContext,
@@ -66,7 +72,11 @@ export function registerOpenid (
router.get('/auth/openid', async (ctx, next) => {
measureCtx.info('try auth via', { provider: 'openid' })
const state = encodeState(ctx, brandings)
const nonce = crypto.randomUUID()
setAuthStateTokenCookie(ctx, nonce)
const state = encodeState(ctx, brandings, nonce)
await passport.authenticate('oidc', {
scope: 'openid profile email',
@@ -79,9 +89,15 @@ export function registerOpenid (
async (ctx, next) => {
const state = safeParseAuthState(ctx.query?.state)
const branding = getBranding(brandings, state?.branding)
const loginUrl = concatLink(branding?.front ?? frontUrl, '/login')
const isValidState = validateAuthStateTokenCookie(ctx, state?.nonce)
if (!isValidState) {
ctx.redirect(loginUrl)
return
}
await passport.authenticate('oidc', {
failureRedirect: concatLink(branding?.front ?? frontUrl, '/login')
failureRedirect: loginUrl
})(ctx, next)
},
async (ctx, next) => {
+27 -2
View File
@@ -32,6 +32,7 @@ export interface AuthState {
branding?: string
autoJoin?: boolean
navigateUrl?: string
nonce?: string
}
export function safeParseAuthState (rawState: string | undefined): AuthState {
@@ -46,14 +47,15 @@ export function safeParseAuthState (rawState: string | undefined): AuthState {
}
}
export function encodeState (ctx: any, brandings: BrandingMap): string {
export function encodeState (ctx: any, brandings: BrandingMap, nonce: string): string {
const host = getHost(ctx.request.headers)
const branding = host !== undefined ? brandings[host]?.key ?? undefined : undefined
const state: AuthState = {
inviteId: ctx.query?.inviteId,
branding,
autoJoin: ctx.query?.autoJoin !== undefined,
navigateUrl: ctx.query?.navigateUrl
navigateUrl: ctx.query?.navigateUrl,
nonce
}
return encodeURIComponent(JSON.stringify(state))
@@ -131,3 +133,26 @@ export async function handleProviderAuth (
return ''
}
}
const authStateTokenCookie = 'auth_state_token'
export function setAuthStateTokenCookie (ctx: any, nonce: string): void {
ctx.cookies.set(authStateTokenCookie, nonce, {
httpOnly: true, // Prevents JavaScript access to cookie
secure: true, // Only sent over HTTPS connections
sameSite: 'lax', // Prevents CSRF by controlling when cookie is sent cross-site
signed: true, // Signs cookie with app.keys to prevent tampering
maxAge: 10 * 60 * 1000 // 10 minutes
})
}
export function validateAuthStateTokenCookie (ctx: any, expectedNonce: string | undefined): boolean {
const nonce = ctx.cookies.get(authStateTokenCookie)
ctx.cookies.set(authStateTokenCookie, null)
if (nonce === undefined || expectedNonce === undefined || nonce !== expectedNonce) {
return false
}
return true
}