Skip to content

Commit c08afc8

Browse files
authored
Add /userinfo endpoint (#160)
1 parent a4982f6 commit c08afc8

File tree

2 files changed

+110
-1
lines changed

2 files changed

+110
-1
lines changed

packages/openauth/src/issuer.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ import { Hono } from "hono/tiny"
130130
import { handle as awsHandle } from "hono/aws-lambda"
131131
import { Context } from "hono"
132132
import { deleteCookie, getCookie, setCookie } from "hono/cookie"
133+
import type { v1 } from "@standard-schema/spec"
133134

134135
/**
135136
* Sets the subject payload in the JWT token and returns the response.
@@ -190,7 +191,7 @@ import {
190191
UnauthorizedClientError,
191192
UnknownStateError,
192193
} from "./error.js"
193-
import { compactDecrypt, CompactEncrypt, SignJWT } from "jose"
194+
import { compactDecrypt, CompactEncrypt, jwtVerify, SignJWT } from "jose"
194195
import { Storage, StorageAdapter } from "./storage/storage.js"
195196
import { encryptionKeys, legacySigningKeys, signingKeys } from "./keys.js"
196197
import { validatePKCE } from "./pkce.js"
@@ -1078,6 +1079,63 @@ export function issuer<
10781079
)
10791080
})
10801081

1082+
app.get("/userinfo", async (c) => {
1083+
const header = c.req.header("Authorization")
1084+
1085+
if (!header) {
1086+
return c.json(
1087+
{
1088+
error: "invalid_request",
1089+
error_description: "Missing Authorization header",
1090+
},
1091+
400,
1092+
)
1093+
}
1094+
1095+
const [type, token] = header.split(" ")
1096+
1097+
if (type !== "Bearer") {
1098+
return c.json(
1099+
{
1100+
error: "invalid_request",
1101+
error_description: "Missing or invalid Authorization header",
1102+
},
1103+
400,
1104+
)
1105+
}
1106+
1107+
if (!token) {
1108+
return c.json(
1109+
{
1110+
error: "invalid_request",
1111+
error_description: "Missing token",
1112+
},
1113+
400,
1114+
)
1115+
}
1116+
1117+
const result = await jwtVerify<{
1118+
mode: "access"
1119+
type: keyof SubjectSchema
1120+
properties: v1.InferInput<SubjectSchema[keyof SubjectSchema]>
1121+
}>(token, () => signingKey.then((item) => item.public), {
1122+
issuer: issuer(c),
1123+
})
1124+
1125+
const validated = await input.subjects[result.payload.type][
1126+
"~standard"
1127+
].validate(result.payload.properties)
1128+
1129+
if (!validated.issues && result.payload.mode === "access") {
1130+
return c.json(validated.value as SubjectSchema)
1131+
}
1132+
1133+
return c.json({
1134+
error: "invalid_token",
1135+
error_description: "Invalid token",
1136+
})
1137+
})
1138+
10811139
app.onError(async (err, c) => {
10821140
console.error(err)
10831141
if (err instanceof UnknownStateError) {

packages/openauth/test/issuer.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,3 +340,54 @@ describe("refresh token", () => {
340340
expect(reused.error).toBe("invalid_request")
341341
})
342342
})
343+
344+
describe("user info", () => {
345+
let tokens: { access: string; refresh: string }
346+
let client: ReturnType<typeof createClient>
347+
348+
const generateTokens = async (issuer: typeof auth) => {
349+
const { challenge, url } = await client.authorize(
350+
"https://client.example.com/callback",
351+
"code",
352+
{ pkce: true },
353+
)
354+
let response = await issuer.request(url)
355+
response = await issuer.request(response.headers.get("location")!, {
356+
headers: {
357+
cookie: response.headers.get("set-cookie")!,
358+
},
359+
})
360+
const location = new URL(response.headers.get("location")!)
361+
const code = location.searchParams.get("code")
362+
const exchanged = await client.exchange(
363+
code!,
364+
"https://client.example.com/callback",
365+
challenge.verifier,
366+
)
367+
if (exchanged.err) throw exchanged.err
368+
return exchanged.tokens
369+
}
370+
371+
const createClientAndTokens = async (issuer: typeof auth) => {
372+
client = createClient({
373+
issuer: "https://auth.example.com",
374+
clientID: "123",
375+
fetch: (a, b) => Promise.resolve(issuer.request(a, b)),
376+
})
377+
tokens = await generateTokens(issuer)
378+
}
379+
380+
beforeEach(async () => {
381+
await createClientAndTokens(auth)
382+
})
383+
384+
test("success", async () => {
385+
const response = await auth.request("https://auth.example.com/userinfo", {
386+
headers: { Authorization: `Bearer ${tokens.access}` },
387+
})
388+
389+
const userinfo = await response.json()
390+
391+
expect(userinfo).toStrictEqual({ userID: "123" })
392+
})
393+
})

0 commit comments

Comments
 (0)