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