@mantiq/oauth 0.5.21 → 0.5.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -36,7 +36,8 @@ export class AuthCodeGrant implements GrantHandler {
|
|
|
36
36
|
|
|
37
37
|
// Verify client secret for confidential clients
|
|
38
38
|
if (client.confidential()) {
|
|
39
|
-
|
|
39
|
+
const storedSecret = client.getAttribute('secret') as string
|
|
40
|
+
if (!clientSecret || !timingSafeEqual(clientSecret, storedSecret)) {
|
|
40
41
|
throw new OAuthError('Invalid client credentials.', 'invalid_client', 401)
|
|
41
42
|
}
|
|
42
43
|
}
|
|
@@ -157,3 +158,20 @@ export class AuthCodeGrant implements GrantHandler {
|
|
|
157
158
|
throw new OAuthError(`Unsupported code challenge method: ${method}`, 'invalid_request')
|
|
158
159
|
}
|
|
159
160
|
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Constant-time string comparison to prevent timing attacks on secret verification.
|
|
164
|
+
*/
|
|
165
|
+
function timingSafeEqual(a: string, b: string): boolean {
|
|
166
|
+
if (a.length !== b.length) return false
|
|
167
|
+
|
|
168
|
+
const encoder = new TextEncoder()
|
|
169
|
+
const bufA = encoder.encode(a)
|
|
170
|
+
const bufB = encoder.encode(b)
|
|
171
|
+
|
|
172
|
+
let result = 0
|
|
173
|
+
for (let i = 0; i < bufA.length; i++) {
|
|
174
|
+
result |= bufA[i]! ^ bufB[i]!
|
|
175
|
+
}
|
|
176
|
+
return result === 0
|
|
177
|
+
}
|
|
@@ -30,8 +30,9 @@ export class ClientCredentialsGrant implements GrantHandler {
|
|
|
30
30
|
const client = await Client.find(clientId)
|
|
31
31
|
if (!client) throw new OAuthError('Client not found.', 'invalid_client', 401)
|
|
32
32
|
|
|
33
|
-
// Verify secret
|
|
34
|
-
|
|
33
|
+
// Verify secret (constant-time comparison to prevent timing attacks)
|
|
34
|
+
const storedSecret = client.getAttribute('secret') as string
|
|
35
|
+
if (!timingSafeEqual(clientSecret, storedSecret)) {
|
|
35
36
|
throw new OAuthError('Invalid client credentials.', 'invalid_client', 401)
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -77,3 +78,20 @@ export class ClientCredentialsGrant implements GrantHandler {
|
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Constant-time string comparison to prevent timing attacks on secret verification.
|
|
84
|
+
*/
|
|
85
|
+
function timingSafeEqual(a: string, b: string): boolean {
|
|
86
|
+
if (a.length !== b.length) return false
|
|
87
|
+
|
|
88
|
+
const encoder = new TextEncoder()
|
|
89
|
+
const bufA = encoder.encode(a)
|
|
90
|
+
const bufB = encoder.encode(b)
|
|
91
|
+
|
|
92
|
+
let result = 0
|
|
93
|
+
for (let i = 0; i < bufA.length; i++) {
|
|
94
|
+
result |= bufA[i]! ^ bufB[i]!
|
|
95
|
+
}
|
|
96
|
+
return result === 0
|
|
97
|
+
}
|
|
@@ -32,9 +32,10 @@ export class RefreshTokenGrant implements GrantHandler {
|
|
|
32
32
|
const client = await Client.find(clientId)
|
|
33
33
|
if (!client) throw new OAuthError('Client not found.', 'invalid_client', 401)
|
|
34
34
|
|
|
35
|
-
// Verify secret for confidential clients
|
|
35
|
+
// Verify secret for confidential clients (constant-time comparison to prevent timing attacks)
|
|
36
36
|
if (client.confidential()) {
|
|
37
|
-
|
|
37
|
+
const storedSecret = client.getAttribute('secret') as string
|
|
38
|
+
if (!clientSecret || !timingSafeEqual(clientSecret, storedSecret)) {
|
|
38
39
|
throw new OAuthError('Invalid client credentials.', 'invalid_client', 401)
|
|
39
40
|
}
|
|
40
41
|
}
|
|
@@ -127,3 +128,20 @@ export class RefreshTokenGrant implements GrantHandler {
|
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Constant-time string comparison to prevent timing attacks on secret verification.
|
|
134
|
+
*/
|
|
135
|
+
function timingSafeEqual(a: string, b: string): boolean {
|
|
136
|
+
if (a.length !== b.length) return false
|
|
137
|
+
|
|
138
|
+
const encoder = new TextEncoder()
|
|
139
|
+
const bufA = encoder.encode(a)
|
|
140
|
+
const bufB = encoder.encode(b)
|
|
141
|
+
|
|
142
|
+
let result = 0
|
|
143
|
+
for (let i = 0; i < bufA.length; i++) {
|
|
144
|
+
result |= bufA[i]! ^ bufB[i]!
|
|
145
|
+
}
|
|
146
|
+
return result === 0
|
|
147
|
+
}
|
|
@@ -78,6 +78,12 @@ export class AuthorizationController {
|
|
|
78
78
|
const client = await Client.find(clientId)
|
|
79
79
|
if (!client) throw new OAuthError('Client not found.', 'invalid_client')
|
|
80
80
|
|
|
81
|
+
// Validate redirect URI matches client's registered URI (same check as GET)
|
|
82
|
+
const allowedRedirect = client.getAttribute('redirect') as string
|
|
83
|
+
if (allowedRedirect && redirectUri !== allowedRedirect) {
|
|
84
|
+
throw new OAuthError('Invalid redirect URI.', 'invalid_request')
|
|
85
|
+
}
|
|
86
|
+
|
|
81
87
|
const userId = typeof user.getAuthIdentifier === 'function'
|
|
82
88
|
? user.getAuthIdentifier()
|
|
83
89
|
: user.id ?? user.getAttribute?.('id')
|
|
@@ -114,11 +120,22 @@ export class AuthorizationController {
|
|
|
114
120
|
* Deny the authorization request.
|
|
115
121
|
*/
|
|
116
122
|
async deny(request: MantiqRequest): Promise<Response> {
|
|
123
|
+
const clientId = await request.input('client_id') as string | undefined
|
|
117
124
|
const redirectUri = await request.input('redirect_uri') as string | undefined
|
|
118
125
|
const state = await request.input('state') as string | undefined
|
|
119
126
|
|
|
127
|
+
if (!clientId) throw new OAuthError('The client_id parameter is required.', 'invalid_request')
|
|
120
128
|
if (!redirectUri) throw new OAuthError('The redirect_uri parameter is required.', 'invalid_request')
|
|
121
129
|
|
|
130
|
+
// Validate redirect URI against client's registered URI (same check as approve)
|
|
131
|
+
const client = await Client.find(clientId)
|
|
132
|
+
if (!client) throw new OAuthError('Client not found.', 'invalid_client')
|
|
133
|
+
|
|
134
|
+
const allowedRedirect = client.getAttribute('redirect') as string
|
|
135
|
+
if (allowedRedirect && redirectUri !== allowedRedirect) {
|
|
136
|
+
throw new OAuthError('Invalid redirect URI.', 'invalid_request')
|
|
137
|
+
}
|
|
138
|
+
|
|
122
139
|
const url = new URL(redirectUri)
|
|
123
140
|
url.searchParams.set('error', 'access_denied')
|
|
124
141
|
url.searchParams.set('error_description', 'The user denied the authorization request.')
|