-
Notifications
You must be signed in to change notification settings - Fork 558
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
624f005
commit e056360
Showing
13 changed files
with
544 additions
and
280 deletions.
There are no files selected for viewing
174 changes: 174 additions & 0 deletions
174
packages/oauth-server/src/authorization/authorization-request-manager.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
import { Client } from '../client/client' | ||
import { ClientId } from '../client/types' | ||
import { DeviceId } from '../device/device-id' | ||
import { | ||
AuthorizationRequest, | ||
AuthorizationRequestJar, | ||
AuthorizationRequestUri, | ||
RequestUri, | ||
RequestUriPrefix, | ||
authorizationRequestSchema, | ||
} from './types' | ||
|
||
// Make opaque | ||
declare const requestIdSymbol: unique symbol | ||
export type RequestId = string & { [requestIdSymbol]: true } | ||
|
||
export type SessionId = string | ||
|
||
export type RequestInfo = { | ||
id: RequestId | ||
uri: RequestUri | ||
data: AuthorizationRequest | ||
expiresAt: Date | ||
} | ||
|
||
export type UpdateData = { | ||
deviceId?: DeviceId | ||
sessionId?: SessionId | ||
expiresAt?: Date | ||
} | ||
|
||
export class AuthorizationRequestManager { | ||
constructor(protected readonly store: unknown) {} | ||
|
||
async create( | ||
client: Client, | ||
input: AuthorizationRequestJar | AuthorizationRequest, | ||
deviceId: null | DeviceId = null, | ||
clientAuthenticated = false, | ||
): Promise<RequestInfo> { | ||
const request = | ||
'request' in input | ||
? authorizationRequestSchema.parse( | ||
( | ||
await client.jwtVerify(input.request, { | ||
maxTokenAge: 60, | ||
}) | ||
).payload, | ||
) | ||
: input | ||
|
||
// TODO Validate request agains client metadata !!!! | ||
|
||
// TODO: insert in store | ||
const data = { | ||
clientId: client.id, | ||
clientAuthenticated, // Is this even used ? | ||
request, | ||
deviceId, | ||
expiresAt: new Date(Date.now() + 60 * 1000), | ||
} | ||
|
||
const id = '123' as RequestId | ||
const uri = encodeRequestUri(id) | ||
return { id, uri, expiresAt: data.expiresAt, data: data.request } | ||
} | ||
|
||
async get( | ||
client: Client, | ||
{ request_uri: uri }: AuthorizationRequestUri, | ||
deviceId: DeviceId, | ||
): Promise<RequestInfo> { | ||
const id = decodeRequestUri(uri) | ||
|
||
// TODO: from a store | ||
const data = { | ||
deviceId: 123n as DeviceId | undefined, | ||
clientId: 'did:web:foo' as ClientId, | ||
sessionId: '123' as SessionId | undefined, | ||
expiresAt: new Date(Date.now() - 60 * 1000 * Math.random()), | ||
request: <AuthorizationRequest>{ | ||
response_type: 'code', | ||
redirect_uri: 'https://foo.com', | ||
scope: 'openid', | ||
state: 'randomStr', | ||
nonce: 'randomStr', | ||
response_mode: 'query', | ||
prompt: 'none', | ||
code_challenge: 'randomStr', | ||
code_challenge_method: 'S256', | ||
}, | ||
} | ||
|
||
const updates: UpdateData = {} | ||
|
||
// TODO : better errors | ||
|
||
if (data.sessionId) { | ||
// If the a session was linked to the request, the next step is to exchange | ||
// the code for a token. | ||
await this.delete(uri) | ||
throw new TypeError('Invalid request_uri') | ||
} | ||
|
||
if (data.expiresAt < new Date()) { | ||
await this.delete(uri) | ||
throw new TypeError('Invalid request_uri') | ||
} else { | ||
updates.expiresAt = new Date(Date.now() + 60 * 1000) | ||
} | ||
|
||
if (data.clientId !== client.id) { | ||
await this.delete(uri) | ||
throw new TypeError('Invalid request_uri') | ||
} | ||
|
||
if (!data.deviceId) { | ||
updates.deviceId = deviceId | ||
} else if (data.deviceId !== deviceId) { | ||
await this.delete(uri) | ||
throw new TypeError('Invalid request_uri') | ||
} | ||
|
||
if (Object.keys(updates).length > 0) { | ||
await this.update(uri, updates) | ||
} | ||
|
||
return { | ||
id, | ||
uri, | ||
expiresAt: updates.expiresAt || data.expiresAt, | ||
data: data.request, | ||
} | ||
} | ||
|
||
async conclude(uri, sessionId: SessionId): Promise<{ code: string }> { | ||
// TODO | ||
|
||
// Bind the request to the session, preventing it from being used again. | ||
// - We might want to update the expiresAt here, to allow some time to the | ||
// client to exchange the code for a token. | ||
|
||
return { code: '123' } | ||
} | ||
|
||
private async update( | ||
requestUri: RequestUri, | ||
data: UpdateData, | ||
): Promise<void> { | ||
// TODO | ||
// Whenever the request is bound to a session, it means that the user has | ||
// already authenticated and consented to the client's request. In this | ||
// case, it should no longer be possible to use the request_uri or update | ||
// the request. | ||
} | ||
|
||
public async delete(uri: RequestUri): Promise<void> { | ||
// TODO | ||
} | ||
} | ||
|
||
function encodeRequestUri(requestId: RequestId): RequestUri { | ||
return `${RequestUriPrefix}${encodeURIComponent(requestId)}` | ||
} | ||
|
||
function decodeRequestUri(requestUri: RequestUri) { | ||
// Foolproofing | ||
if (!requestUri.startsWith(RequestUriPrefix)) { | ||
throw new TypeError('Invalid request_uri') | ||
} | ||
return decodeURIComponent( | ||
requestUri.slice(RequestUriPrefix.length), | ||
) as RequestId | ||
} |
Oops, something went wrong.