@@ -5,122 +5,101 @@ import {
5
5
RouteResponse ,
6
6
} from "../module.gen.ts" ;
7
7
8
- import { getCodeVerifierFromCookie , getStateFromCookie , getLoginIdFromCookie } from "../utils/trace.ts" ;
9
8
import { getFullConfig } from "../utils/env.ts" ;
10
9
import { getClient } from "../utils/client.ts" ;
11
10
import { getUserUniqueIdentifier } from "../utils/client.ts" ;
12
11
import { Tokens } from "https://deno.land/x/oauth2_client@v1.0.2/mod.ts" ;
13
12
13
+ import { compareConstantTime , stateToDataStr } from "../utils/state.ts" ;
14
+ import { OAUTH_DONE_HTML } from "../utils/pages.ts" ;
15
+
14
16
export async function handle (
15
17
ctx : RouteContext ,
16
18
req : RouteRequest ,
17
19
) : Promise < RouteResponse > {
18
- // Max 2 login attempts per IP per minute
20
+ // Max 5 login attempts per IP per minute
19
21
ctx . modules . rateLimit . throttlePublic ( { requests : 5 , period : 60 } ) ;
20
22
21
23
// Ensure that the provider configurations are valid
22
- const config = await getFullConfig ( ctx . userConfig ) ;
24
+ const config = await getFullConfig ( ctx . config ) ;
23
25
if ( ! config ) throw new RuntimeError ( "invalid_config" , { statusCode : 500 } ) ;
24
26
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 ) ;
28
29
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
+ }
30
35
36
+ // Extract the data from the state
37
+ const stateData = await stateToDataStr ( config . oauthSecret , redirectedState ) ;
38
+ const { flowId, providerId } = JSON . parse ( stateData ) ;
31
39
32
40
// 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
+ } ,
35
45
} ) ;
36
-
37
46
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 ;
40
65
41
66
// Get the provider config
42
- const provider = config . providers [ loginAttempt . provider ] ;
67
+ const provider = config . providers [ providerId ] ;
43
68
if ( ! provider ) throw new RuntimeError ( "invalid_provider" , { statusCode : 400 } ) ;
44
69
45
70
// Get the oauth client
46
- const client = getClient ( config , provider . name , new URL ( req . url ) ) ;
71
+ const client = getClient ( config , provider . name ) ;
47
72
if ( ! client . config . redirectUri ) throw new RuntimeError ( "invalid_config" , { statusCode : 500 } ) ;
48
73
49
-
50
- // Get the URI that this request was made to
51
- const uri = new URL ( req . url ) ;
52
- const uriStr = uri . toString ( ) ;
53
-
54
74
// Get the user's tokens and sub
55
75
let tokens : Tokens ;
56
- let sub : string ;
76
+ let ident : string ;
57
77
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 ) ;
60
80
} catch ( e ) {
61
81
console . error ( e ) ;
62
82
throw new RuntimeError ( "invalid_oauth_response" , { statusCode : 502 } ) ;
63
83
}
64
84
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 ( {
70
87
where : {
71
- sub,
72
- provider : loginAttempt . provider ,
88
+ id : flowId ,
73
89
} ,
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 ( {
97
90
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 ( ) ,
104
93
} ,
105
94
} ) ;
106
95
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
+ ) ;
126
105
}
0 commit comments