Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OAuth2 scope support #156

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/new-donuts-tap.md
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is right. Haven't used this before either.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@openauthjs/openauth": minor
---

Add support for OAuth2 scope
23 changes: 20 additions & 3 deletions packages/openauth/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,22 @@
*
* @packageDocumentation
*/
import type { v1 } from "@standard-schema/spec"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These got reordered because of Organize Imports. Let me know if you want me to revert this part.

import {
createLocalJWKSet,
decodeJwt,
errors,
JSONWebKeySet,
jwtVerify,
decodeJwt,
} from "jose"
import { SubjectSchema } from "./subject.js"
import type { v1 } from "@standard-schema/spec"
import {
InvalidAccessTokenError,
InvalidAuthorizationCodeError,
InvalidRefreshTokenError,
InvalidSubjectError,
} from "./error.js"
import { generatePKCE } from "./pkce.js"
import { SubjectSchema } from "./subject.js"

/**
* The well-known information for an OAuth 2.0 authorization server.
Expand Down Expand Up @@ -173,6 +173,16 @@ export interface AuthorizeOptions {
* If there's only one provider configured, the user will be redirected to that.
*/
provider?: string
/**
* The scopes you want to request.
*
* @example
* ```ts
* {
* scopes: ["read", "write"]
* }
*/
scopes?: string[]
}

export interface AuthorizeResult {
Expand Down Expand Up @@ -316,6 +326,10 @@ export interface VerifyResult<T extends SubjectSchema> {
subject: {
[type in keyof T]: { type: type; properties: v1.InferOutput<T[type]> }
}[keyof T]
/**
* The scopes of the token.
*/
scopes?: string[]
}

/**
Expand Down Expand Up @@ -589,6 +603,7 @@ export function createClient(input: ClientInput): Client {
result.searchParams.set("code_challenge", pkce.challenge)
challenge.verifier = pkce.verifier
}
if (opts?.scopes) result.searchParams.set("scope", opts.scopes.join(" "))
return {
challenge,
url: result.toString(),
Expand Down Expand Up @@ -698,6 +713,7 @@ export function createClient(input: ClientInput): Client {
mode: "access"
type: keyof T
properties: v1.InferInput<T[keyof T]>
scopes?: string[]
}>(token, jwks, {
issuer,
})
Expand All @@ -711,6 +727,7 @@ export function createClient(input: ClientInput): Client {
type: result.payload.type,
properties: validated.value,
} as any,
...(result.payload.scopes ? { scopes: result.payload.scopes } : {}),
}
return {
err: new InvalidSubjectError(),
Expand Down
46 changes: 37 additions & 9 deletions packages/openauth/src/issuer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,12 @@
*
* @packageDocumentation
*/
import { Provider, ProviderOptions } from "./provider/provider.js"
import { SubjectPayload, SubjectSchema } from "./subject.js"
import { Hono } from "hono/tiny"
import { handle as awsHandle } from "hono/aws-lambda"
import { Context } from "hono"
import { handle as awsHandle } from "hono/aws-lambda"
import { deleteCookie, getCookie, setCookie } from "hono/cookie"
import { Hono } from "hono/tiny"
import { Provider, ProviderOptions } from "./provider/provider.js"
import { SubjectPayload, SubjectSchema } from "./subject.js"

/**
* Sets the subject payload in the JWT token and returns the response.
Expand Down Expand Up @@ -171,6 +171,7 @@ export interface AuthorizationState {
state: string
client_id: string
audience?: string
scopes?: string[]
pkce?: {
challenge: string
method: "S256"
Expand All @@ -184,22 +185,23 @@ export type Prettify<T> = {
[K in keyof T]: T[K]
} & {}

import { cors } from "hono/cors"
import { compactDecrypt, CompactEncrypt, SignJWT } from "jose"
import {
MissingParameterError,
OauthError,
UnauthorizedClientError,
UnknownStateError,
} from "./error.js"
import { compactDecrypt, CompactEncrypt, SignJWT } from "jose"
import { Storage, StorageAdapter } from "./storage/storage.js"
import { encryptionKeys, legacySigningKeys, signingKeys } from "./keys.js"
import { validatePKCE } from "./pkce.js"
import { parseScopes, validateScopes } from "./scopes.js"
import { DynamoStorage } from "./storage/dynamo.js"
import { MemoryStorage } from "./storage/memory.js"
import { Storage, StorageAdapter } from "./storage/storage.js"
import { Select } from "./ui/select.js"
import { setTheme, Theme } from "./ui/theme.js"
import { isDomainMatch } from "./util.js"
import { DynamoStorage } from "./storage/dynamo.js"
import { MemoryStorage } from "./storage/memory.js"
import { cors } from "hono/cors"

/** @internal */
export const aws = awsHandle
Expand Down Expand Up @@ -279,6 +281,17 @@ export interface IssuerInput<
* ```
*/
providers: Providers
/**
* Array containing a list of the OAuth 2.0 [RFC6749] "scope" values that this authorization server advertises.
*
* @example
* ```ts
* {
* scopes_supported: ["read", "write"]
* }
* ```
*/
scopes_supported?: string[]
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would also be a good place to add default_scopes or so, which would be used if the auth request has no scope.

/**
* The theme you want to use for the UI.
*
Expand Down Expand Up @@ -524,6 +537,7 @@ export function issuer<
type: type as string,
properties,
clientID: authorization.client_id,
scopes: authorization.scopes,
ttl: {
access: subjectOpts?.ttl?.access ?? ttlAccess,
refresh: subjectOpts?.ttl?.refresh ?? ttlRefresh,
Expand All @@ -549,6 +563,7 @@ export function issuer<
redirectURI: authorization.redirect_uri,
clientID: authorization.client_id,
pkce: authorization.pkce,
scopes: authorization.scopes,
ttl: {
access: subjectOpts?.ttl?.access ?? ttlAccess,
refresh: subjectOpts?.ttl?.refresh ?? ttlRefresh,
Expand Down Expand Up @@ -653,6 +668,7 @@ export function issuer<
}
timeUsed?: number
nextToken?: string
scopes?: string[]
},
opts?: {
generateRefreshToken?: boolean
Expand Down Expand Up @@ -686,6 +702,7 @@ export function issuer<
aud: value.clientID,
iss: issuer(ctx),
sub: value.subject,
scopes: value.scopes,
})
.setExpirationTime(
Math.floor((value.timeUsed ?? Date.now()) / 1000 + value.ttl.access),
Expand Down Expand Up @@ -775,6 +792,7 @@ export function issuer<
token_endpoint: `${iss}/token`,
jwks_uri: `${iss}/.well-known/jwks.json`,
response_types_supported: ["code", "token"],
scopes_supported: input.scopes_supported,
})
},
)
Expand All @@ -790,6 +808,7 @@ export function issuer<
async (c) => {
const form = await c.req.formData()
const grantType = form.get("grant_type")
const scope = form.get("scope") as string | null

if (grantType === "authorization_code") {
const code = form.get("code")
Expand All @@ -808,6 +827,7 @@ export function issuer<
clientID: string
redirectURI: string
subject: string
scopes?: string[]
ttl: {
access: number
refresh: number
Expand Down Expand Up @@ -871,10 +891,12 @@ export function issuer<
)
}
}
payload.scopes = validateScopes(scope, payload.scopes)
const tokens = await generateTokens(c, payload)
return c.json({
access_token: tokens.access,
refresh_token: tokens.refresh,
scope: payload.scopes?.join(" "),
})
}

Expand All @@ -897,6 +919,7 @@ export function issuer<
properties: any
clientID: string
subject: string
scopes?: string[]
ttl: {
access: number
refresh: number
Expand Down Expand Up @@ -936,12 +959,14 @@ export function issuer<
400,
)
}
payload.scopes = validateScopes(scope, payload.scopes)
const tokens = await generateTokens(c, payload, {
generateRefreshToken,
})
return c.json({
access_token: tokens.access,
refresh_token: tokens.refresh,
scope: payload.scopes?.join(" "),
})
}

Expand Down Expand Up @@ -977,6 +1002,7 @@ export function issuer<
opts?.subject || (await resolveSubject(type, properties)),
properties,
clientID: clientID.toString(),
scopes: parseScopes(scope),
ttl: {
access: opts?.ttl?.access ?? ttlAccess,
refresh: opts?.ttl?.refresh ?? ttlRefresh,
Expand Down Expand Up @@ -1009,12 +1035,14 @@ export function issuer<
const audience = c.req.query("audience")
const code_challenge = c.req.query("code_challenge")
const code_challenge_method = c.req.query("code_challenge_method")
const scope = c.req.query("scope")
const authorization: AuthorizationState = {
response_type,
redirect_uri,
state,
client_id,
audience,
scopes: parseScopes(scope),
pkce:
code_challenge && code_challenge_method
? {
Expand Down
10 changes: 10 additions & 0 deletions packages/openauth/src/scopes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function parseScopes(scope: string | null | undefined) {
return scope?.split(" ").filter((s) => s)
}

export function validateScopes(tokenReq?: string | null, authorizeReq?: string[]) {
if (!authorizeReq?.length || tokenReq === null || tokenReq === undefined) {
return authorizeReq
}
return [...new Set(parseScopes(tokenReq)).intersection(new Set(authorizeReq))]
}
Loading