@@ -5,121 +5,100 @@ import {
55 RouteResponse ,
66} from "../module.gen.ts" ;
77
8- import { getCodeVerifierFromCookie , getStateFromCookie , getLoginIdFromCookie } from "../utils/trace.ts" ;
98import { getFullConfig } from "../utils/env.ts" ;
109import { getClient } from "../utils/client.ts" ;
1110import { getUserUniqueIdentifier } from "../utils/client.ts" ;
1211
12+ import { compareConstantTime , stateToDataStr } from "../utils/state.ts" ;
13+ import { OAUTH_DONE_HTML } from "../utils/pages.ts" ;
14+
1315export async function handle (
1416 ctx : RouteContext ,
1517 req : RouteRequest ,
1618) : Promise < RouteResponse > {
17- // Max 2 login attempts per IP per minute
19+ // Max 5 login attempts per IP per minute
1820 ctx . modules . rateLimit . throttlePublic ( { requests : 5 , period : 60 } ) ;
1921
2022 // Ensure that the provider configurations are valid
21- const config = await getFullConfig ( ctx . userConfig ) ;
23+ const config = await getFullConfig ( ctx . config ) ;
2224 if ( ! config ) throw new RuntimeError ( "invalid_config" , { statusCode : 500 } ) ;
2325
24- const loginId = getLoginIdFromCookie ( ctx ) ;
25- const codeVerifier = getCodeVerifierFromCookie ( ctx ) ;
26- const state = getStateFromCookie ( ctx ) ;
26+ // Get the URI that this request was made to
27+ const uri = new URL ( req . url ) ;
2728
28- if ( ! loginId || ! codeVerifier || ! state ) throw new RuntimeError ( "missing_login_data" , { statusCode : 400 } ) ;
29+ // Get the state from the URI
30+ const redirectedState = uri . searchParams . get ( "state" ) ;
31+ if ( ! redirectedState ) {
32+ throw new RuntimeError ( "missing_state" , { statusCode : 400 } ) ;
33+ }
2934
35+ // Extract the data from the state
36+ const stateData = await stateToDataStr ( config . oauthSecret , redirectedState ) ;
37+ const { flowId, providerId } = JSON . parse ( stateData ) ;
3038
3139 // Get the login attempt stored in the database
32- const loginAttempt = await ctx . db . oAuthLoginAttempt . findUnique ( {
33- where : { id : loginId , completedAt : null , invalidatedAt : null } ,
40+ const loginAttempt = await ctx . db . loginAttempts . findUnique ( {
41+ where : {
42+ id : flowId ,
43+ } ,
3444 } ) ;
35-
3645 if ( ! loginAttempt ) throw new RuntimeError ( "login_not_found" , { statusCode : 400 } ) ;
37- if ( loginAttempt . state !== state ) throw new RuntimeError ( "invalid_state" , { statusCode : 400 } ) ;
38- if ( loginAttempt . codeVerifier !== codeVerifier ) throw new RuntimeError ( "invalid_code_verifier" , { statusCode : 400 } ) ;
46+
47+ // Check if the login attempt is valid
48+ if ( loginAttempt . completedAt ) {
49+ throw new RuntimeError ( "login_already_completed" , { statusCode : 400 } ) ;
50+ }
51+ if ( loginAttempt . invalidatedAt ) {
52+ throw new RuntimeError ( "login_cancelled" , { statusCode : 400 } ) ;
53+ }
54+ if ( new Date ( loginAttempt . expiresAt ) < new Date ( ) ) {
55+ throw new RuntimeError ( "login_expired" , { statusCode : 400 } ) ;
56+ }
57+
58+ // Check if the provider ID and state match
59+ const providerIdMatch = compareConstantTime ( loginAttempt . providerId , providerId ) ;
60+ const stateMatch = compareConstantTime ( loginAttempt . state , redirectedState ) ;
61+ if ( ! providerIdMatch || ! stateMatch ) throw new RuntimeError ( "invalid_state" , { statusCode : 400 } ) ;
62+
63+ const { state, codeVerifier } = loginAttempt ;
3964
4065 // Get the provider config
41- const provider = config . providers [ loginAttempt . provider ] ;
66+ const provider = config . providers [ providerId ] ;
4267 if ( ! provider ) throw new RuntimeError ( "invalid_provider" , { statusCode : 400 } ) ;
4368
4469 // Get the oauth client
45- const client = getClient ( config , provider . name , new URL ( req . url ) ) ;
70+ const client = getClient ( config , provider . name ) ;
4671 if ( ! client . config . redirectUri ) throw new RuntimeError ( "invalid_config" , { statusCode : 500 } ) ;
4772
48-
49- // Get the URI that this request was made to
50- const uri = new URL ( req . url ) ;
51- const uriStr = uri . toString ( ) ;
52-
5373 // Get the user's tokens and sub
5474 let tokens : Awaited < ReturnType < typeof client . code . getToken > > ;
55- let sub : string ;
75+ let ident : string ;
5676 try {
57- tokens = await client . code . getToken ( uriStr , { state, codeVerifier } ) ;
58- sub = await getUserUniqueIdentifier ( tokens . accessToken , provider ) ;
77+ tokens = await client . code . getToken ( uri . toString ( ) , { state, codeVerifier } ) ;
78+ ident = await getUserUniqueIdentifier ( tokens . accessToken , provider ) ;
5979 } catch ( e ) {
6080 console . error ( e ) ;
6181 throw new RuntimeError ( "invalid_oauth_response" , { statusCode : 502 } ) ;
6282 }
6383
64- const expiresIn = tokens . expiresIn ?? 3600 ;
65- const expiry = new Date ( Date . now ( ) + expiresIn ) ;
66-
67- // Ensure the user is registered with this sub/provider combo
68- const user = await ctx . db . oAuthUsers . findFirst ( {
84+ // Update the login attempt
85+ await ctx . db . loginAttempts . update ( {
6986 where : {
70- sub,
71- provider : loginAttempt . provider ,
87+ id : flowId ,
7288 } ,
73- } ) ;
74-
75- let userId : string ;
76- if ( user ) {
77- userId = user . userId ;
78- } else {
79- const { user : newUser } = await ctx . modules . users . createUser ( { username : sub } ) ;
80- await ctx . db . oAuthUsers . create ( {
81- data : {
82- sub,
83- provider : loginAttempt . provider ,
84- userId : newUser . id ,
85- } ,
86- } ) ;
87-
88- userId = newUser . id ;
89- }
90-
91- // Generate a token which the user can use to authenticate with this module
92- const { token } = await ctx . modules . users . createUserToken ( { userId } ) ;
93-
94- // Record the credentials
95- await ctx . db . oAuthCreds . create ( {
9689 data : {
97- loginAttemptId : loginAttempt . id ,
98- provider : provider . name ,
99- accessToken : tokens . accessToken ,
100- refreshToken : tokens . refreshToken ?? "" ,
101- userToken : token . token ,
102- expiresAt : expiry ,
90+ identifier : ident ,
91+ completedAt : new Date ( ) ,
10392 } ,
10493 } ) ;
10594
106-
107- const response = RouteResponse . redirect ( loginAttempt . targetUrl , 303 ) ;
108-
109- const headers = new Headers ( response . headers ) ;
110-
111- // Clear login session cookies
112- const expireAttribs = `Path=/; Max-Age=0; SameSite=Lax; Expires=${ new Date ( 0 ) . toUTCString ( ) } ` ;
113- headers . append ( "Set-Cookie" , `login_id=EXPIRED; ${ expireAttribs } ` ) ;
114- headers . append ( "Set-Cookie" , `code_verifier=EXPIRED; ${ expireAttribs } ` ) ;
115- headers . append ( "Set-Cookie" , `state=EXPIRED; ${ expireAttribs } ` ) ;
116-
117- // Tell the browser to never cache this page
118- headers . set ( "Cache-Control" , "no-store" ) ;
119-
120- // Set token cookie
121- const cookieAttribs = `Path=/; Max-Age=${ expiresIn } ; SameSite=Lax; Expires=${ expiry . toUTCString ( ) } ` ;
122- headers . append ( "Set-Cookie" , `token=${ token . token } ; ${ cookieAttribs } ` ) ;
123-
124- return new Response ( response . body , { status : response . status , headers } ) ;
95+ return new RouteResponse (
96+ OAUTH_DONE_HTML ,
97+ {
98+ status : 200 ,
99+ headers : {
100+ "Content-Type" : "text/html" ,
101+ } ,
102+ } ,
103+ ) ;
125104}
0 commit comments