honojs-middleware/packages/oauth-providers/src/providers/msentra/authFlow.ts

125 lines
3.2 KiB
TypeScript

import { HTTPException } from 'hono/http-exception'
import { toQueryParams } from '../../utils/objectToQuery'
import type { MSEntraErrorResponse, MSEntraToken, MSEntraTokenResponse, MSEntraUser } from './types'
type MSEntraAuthFlow = {
client_id: string
client_secret: string
tenant_id: string
redirect_uri: string
code: string | undefined
token: MSEntraToken | undefined
scope: string[]
state?: string
}
export class AuthFlow {
client_id: string
client_secret: string
tenant_id: string
redirect_uri: string
code: string | undefined
token: MSEntraToken | undefined
scope: string[]
state: string | undefined
user: Partial<MSEntraUser> | undefined
granted_scopes: string[] | undefined
constructor({
client_id,
client_secret,
tenant_id,
redirect_uri,
code,
token,
scope,
state,
}: MSEntraAuthFlow) {
this.client_id = client_id
this.client_secret = client_secret
this.tenant_id = tenant_id
this.redirect_uri = redirect_uri
this.code = code
this.token = token
this.scope = scope
this.state = state
this.user = undefined
if (
this.client_id === undefined ||
this.client_secret === undefined ||
this.tenant_id === undefined ||
this.scope.length <= 0
) {
throw new HTTPException(400, {
message: 'Required parameters were not found. Please provide them to proceed.',
})
}
}
redirect() {
const parsedOptions = toQueryParams({
response_type: 'code',
redirect_uri: this.redirect_uri,
client_id: this.client_id,
include_granted_scopes: true,
scope: this.scope.join(' '),
state: this.state,
})
return `https://login.microsoft.com/${this.tenant_id}/oauth2/v2.0/authorize?${parsedOptions}`
}
async getTokenFromCode(): Promise<void> {
const parsedOptions = toQueryParams({
client_id: this.client_id,
client_secret: this.client_secret,
redirect_uri: this.redirect_uri,
code: this.code,
grant_type: 'authorization_code',
})
const response = (await fetch(
`https://login.microsoft.com/${this.tenant_id}/oauth2/v2.0/token`,
{
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
body: parsedOptions,
}
).then((res) => res.json())) as MSEntraTokenResponse | MSEntraErrorResponse
if ('error' in response) {
throw new HTTPException(400, { message: response.error })
}
if ('access_token' in response) {
this.token = {
token: response.access_token,
expires_in: response.expires_in,
refresh_token: response.refresh_token,
}
this.granted_scopes = response.scope.split(' ')
}
}
async getUserData(): Promise<void> {
await this.getTokenFromCode()
//TODO: add support for extra fields
const response = (await fetch('https://graph.microsoft.com/v1.0/me', {
headers: {
authorization: `Bearer ${this.token?.token}`,
},
}).then(async (res) => res.json())) as MSEntraUser | MSEntraErrorResponse
if ('error' in response) {
throw new HTTPException(400, { message: response.error })
}
if ('id' in response) {
this.user = response
}
}
}