From 376d8230cf4afaff1f97914b1f9ef6184ffba8a5 Mon Sep 17 00:00:00 2001 From: Markus Ahlstrand Date: Mon, 9 Dec 2024 08:08:37 +0100 Subject: [PATCH] add silent auth for the code flow --- apps/demo/CHANGELOG.md | 8 +++ apps/demo/package.json | 6 +- packages/adapter-interfaces/CHANGELOG.md | 6 ++ packages/adapter-interfaces/package.json | 2 +- .../src/types/AuthParams.ts | 4 +- packages/authhero/CHANGELOG.md | 11 ++++ packages/authhero/package.json | 2 +- .../authorization-code.ts | 34 +++++++++- .../client-credentials.ts | 3 +- .../src/authentication-flows/common.ts | 1 + packages/authhero/src/constants.ts | 2 + packages/authhero/src/routes/oauth2/token.ts | 17 +++-- .../authhero/test/routes/oauth2/token.spec.ts | 63 ++++++++++++++++++- packages/drizzle/CHANGELOG.md | 7 +++ packages/drizzle/package.json | 2 +- packages/kysely/CHANGELOG.md | 7 +++ packages/kysely/package.json | 2 +- 17 files changed, 153 insertions(+), 24 deletions(-) diff --git a/apps/demo/CHANGELOG.md b/apps/demo/CHANGELOG.md index 0574efa..30ae7e9 100644 --- a/apps/demo/CHANGELOG.md +++ b/apps/demo/CHANGELOG.md @@ -1,5 +1,13 @@ # @authhero/demo +## 0.2.6 + +### Patch Changes + +- Updated dependencies + - authhero@0.17.0 + - @authhero/kysely-adapter@0.24.2 + ## 0.2.5 ### Patch Changes diff --git a/apps/demo/package.json b/apps/demo/package.json index bd2e60c..c1923a1 100644 --- a/apps/demo/package.json +++ b/apps/demo/package.json @@ -1,15 +1,15 @@ { "name": "@authhero/demo", "private": true, - "version": "0.2.5", + "version": "0.2.6", "scripts": { "dev": "bun --watch src/bun.ts" }, "dependencies": { - "@authhero/kysely-adapter": "^0.24.1", + "@authhero/kysely-adapter": "^0.24.2", "@hono/swagger-ui": "^0.4.1", "@hono/zod-openapi": "^0.18.0", - "authhero": "^0.16.0", + "authhero": "^0.17.0", "hono": "^4.6.11", "hono-openapi-middlewares": "^1.0.11", "kysely-bun-sqlite": "^0.3.2" diff --git a/packages/adapter-interfaces/CHANGELOG.md b/packages/adapter-interfaces/CHANGELOG.md index 9815e60..88f2892 100644 --- a/packages/adapter-interfaces/CHANGELOG.md +++ b/packages/adapter-interfaces/CHANGELOG.md @@ -1,5 +1,11 @@ # @authhero/adapter-interfaces +## 0.29.0 + +### Minor Changes + +- add silent tokens + ## 0.28.0 ### Minor Changes diff --git a/packages/adapter-interfaces/package.json b/packages/adapter-interfaces/package.json index f2a4305..33c8f09 100644 --- a/packages/adapter-interfaces/package.json +++ b/packages/adapter-interfaces/package.json @@ -11,7 +11,7 @@ "type": "git", "url": "https://github.com/markusahlstrand/authhero" }, - "version": "0.28.0", + "version": "0.29.0", "files": [ "dist" ], diff --git a/packages/adapter-interfaces/src/types/AuthParams.ts b/packages/adapter-interfaces/src/types/AuthParams.ts index acfd872..8867b10 100644 --- a/packages/adapter-interfaces/src/types/AuthParams.ts +++ b/packages/adapter-interfaces/src/types/AuthParams.ts @@ -15,8 +15,8 @@ export enum AuthorizationResponseMode { } export enum CodeChallengeMethod { - S265 = "S256", - plain = "plain", + S256 = "S256", + Plain = "plain", } export const authParamsSchema = z.object({ diff --git a/packages/authhero/CHANGELOG.md b/packages/authhero/CHANGELOG.md index ef5d94f..3807fbf 100644 --- a/packages/authhero/CHANGELOG.md +++ b/packages/authhero/CHANGELOG.md @@ -1,5 +1,16 @@ # authhero +## 0.17.0 + +### Minor Changes + +- add silent tokens + +### Patch Changes + +- Updated dependencies + - @authhero/adapter-interfaces@0.29.0 + ## 0.16.0 ### Minor Changes diff --git a/packages/authhero/package.json b/packages/authhero/package.json index 32b8efa..bbe7fa3 100644 --- a/packages/authhero/package.json +++ b/packages/authhero/package.json @@ -1,6 +1,6 @@ { "name": "authhero", - "version": "0.16.0", + "version": "0.17.0", "files": [ "dist" ], diff --git a/packages/authhero/src/authentication-flows/authorization-code.ts b/packages/authhero/src/authentication-flows/authorization-code.ts index ee28668..f81975f 100644 --- a/packages/authhero/src/authentication-flows/authorization-code.ts +++ b/packages/authhero/src/authentication-flows/authorization-code.ts @@ -1,9 +1,12 @@ import { HTTPException } from "hono/http-exception"; import { Context } from "hono"; import { z } from "@hono/zod-openapi"; +import { nanoid } from "nanoid"; import { createAuthTokens } from "./common"; import { Bindings, Variables } from "../types"; import { computeCodeChallenge } from "../utils/crypto"; +import { SILENT_AUTH_MAX_AGE, SILENT_COOKIE_NAME } from "../constants"; +import { serializeCookie } from "oslo/cookie"; export const authorizationCodeGrantParamsSchema = z .object({ @@ -97,5 +100,34 @@ export async function authorizationCodeGrant( await ctx.env.data.codes.remove(client.tenant.id, params.code); - return createAuthTokens(ctx, { authParams: login.authParams, user }); + // Create a new session + const session = await ctx.env.data.sessions.create(client.tenant.id, { + session_id: nanoid(), + user_id: user.user_id, + client_id: client.id, + expires_at: new Date(Date.now() + SILENT_AUTH_MAX_AGE * 1000).toISOString(), + used_at: new Date().toISOString(), + }); + + const tokens = await createAuthTokens(ctx, { + authParams: login.authParams, + user, + sid: session.session_id, + }); + + return ctx.json(tokens, { + headers: { + "set-cookie": serializeCookie( + `${client.tenant.id}-${SILENT_COOKIE_NAME}`, + session.session_id, + { + path: "/", + httpOnly: true, + secure: true, + maxAge: 60 * 60 * 24 * 7, // 1 mo + sameSite: "none", + }, + ), + }, + }); } diff --git a/packages/authhero/src/authentication-flows/client-credentials.ts b/packages/authhero/src/authentication-flows/client-credentials.ts index 8e2a547..90ef9f3 100644 --- a/packages/authhero/src/authentication-flows/client-credentials.ts +++ b/packages/authhero/src/authentication-flows/client-credentials.ts @@ -37,5 +37,6 @@ export async function clientCredentialsGrant( audience: params.audience, }; - return createAuthTokens(ctx, { authParams }); + const tokens = await createAuthTokens(ctx, { authParams }); + return ctx.json(tokens); } diff --git a/packages/authhero/src/authentication-flows/common.ts b/packages/authhero/src/authentication-flows/common.ts index 89f018c..1828369 100644 --- a/packages/authhero/src/authentication-flows/common.ts +++ b/packages/authhero/src/authentication-flows/common.ts @@ -39,6 +39,7 @@ export async function createAuthTokens( sub: user?.user_id || authParams.client_id, iss: ctx.env.ISSUER, tenant_id: ctx.var.tenant_id, + sid, }, { includeIssuedTimestamp: true, diff --git a/packages/authhero/src/constants.ts b/packages/authhero/src/constants.ts index 52acb1e..d4e4ee8 100644 --- a/packages/authhero/src/constants.ts +++ b/packages/authhero/src/constants.ts @@ -1 +1,3 @@ export const JWKS_CACHE_TIMEOUT_IN_SECONDS = 60 * 5; // 5 minutes +export const SILENT_AUTH_MAX_AGE = 30 * 24 * 60 * 60; // 30 days +export const SILENT_COOKIE_NAME = "auth-token"; diff --git a/packages/authhero/src/routes/oauth2/token.ts b/packages/authhero/src/routes/oauth2/token.ts index 8afaefd..65267a6 100644 --- a/packages/authhero/src/routes/oauth2/token.ts +++ b/packages/authhero/src/routes/oauth2/token.ts @@ -88,6 +88,7 @@ export const tokenRoutes = new OpenAPIHono<{ }, }, }), + // @ts-ignore async (ctx) => { const body = ctx.req.valid("form"); @@ -100,18 +101,14 @@ export const tokenRoutes = new OpenAPIHono<{ switch (body.grant_type) { case GrantType.AuthorizationCode: - return ctx.json( - await authorizationCodeGrant( - ctx, - authorizationCodeGrantParamsSchema.parse(params), - ), + return authorizationCodeGrant( + ctx, + authorizationCodeGrantParamsSchema.parse(params), ); case GrantType.ClientCredential: - return ctx.json( - await clientCredentialsGrant( - ctx, - clientCredentialGrantParamsSchema.parse(params), - ), + return clientCredentialsGrant( + ctx, + clientCredentialGrantParamsSchema.parse(params), ); default: throw new HTTPException(400, { message: "Not implemented" }); diff --git a/packages/authhero/test/routes/oauth2/token.spec.ts b/packages/authhero/test/routes/oauth2/token.spec.ts index d38d10e..9c69362 100644 --- a/packages/authhero/test/routes/oauth2/token.spec.ts +++ b/packages/authhero/test/routes/oauth2/token.spec.ts @@ -3,6 +3,7 @@ import { testClient } from "hono/testing"; import { getTestServer } from "../../helpers/test-server"; import { parseJWT } from "oslo/jwt"; import { computeCodeChallenge } from "../../../src/utils/crypto"; +import { CodeChallengeMethod } from "@authhero/adapter-interfaces"; describe("token", () => { describe("client_credentials", () => { @@ -446,6 +447,62 @@ describe("token", () => { expect(secondResponse.status).toBe(403); }); + + it("should set a silent authentication token", async () => { + const { oauthApp, env } = await getTestServer(); + const client = testClient(oauthApp, env); + + // Create the login session and code + const loginSesssion = await env.data.logins.create("tenantId", { + expires_at: new Date(Date.now() + 1000 * 60 * 5).toISOString(), + authParams: { + client_id: "clientId", + username: "foo@example.com", + scope: "", + audience: "http://example.com", + redirect_uri: "http://example.com/callback", + }, + }); + + await env.data.codes.create("tenantId", { + code_type: "authorization_code", + user_id: "email|userId", + code_id: "123456", + login_id: loginSesssion.login_id, + expires_at: new Date(Date.now() + 1000 * 60 * 5).toISOString(), + }); + + const response = await client.oauth.token.$post( + { + form: { + grant_type: "authorization_code", + code: "123456", + redirect_uri: "http://example.com/callback", + client_id: "clientId", + client_secret: "clientSecret", + }, + }, + { + headers: { + "tenant-id": "tenantId", + }, + }, + ); + + expect(response.status).toBe(200); + const body = await response.json(); + + const accessToken = parseJWT(body.access_token); + + if (!accessToken || !("sid" in accessToken.payload)) { + throw new Error("sid is missing"); + } + + const cookie = response.headers.get("set-cookie"); + expect(cookie).toBe( + `tenantId-auth-token=${accessToken?.payload.sid}; HttpOnly; Max-Age=604800; Path=/; SameSite=None; Secure`, + ); + }); }); describe("authorization_code with PKCE", () => { @@ -465,7 +522,7 @@ describe("token", () => { scope: "", audience: "http://example.com", code_challenge: codeChallenge, - code_challenge_method: "plain", + code_challenge_method: CodeChallengeMethod.Plain, }, }); @@ -523,7 +580,7 @@ describe("token", () => { scope: "", audience: "http://example.com", code_challenge: await computeCodeChallenge(codeChallenge, "S256"), - code_challenge_method: "S256", + code_challenge_method: CodeChallengeMethod.S256, }, }); @@ -581,7 +638,7 @@ describe("token", () => { scope: "", audience: "http://example.com", code_challenge: codeChallenge, - code_challenge_method: "plain", + code_challenge_method: CodeChallengeMethod.Plain, }, }); diff --git a/packages/drizzle/CHANGELOG.md b/packages/drizzle/CHANGELOG.md index e0161a5..841573d 100644 --- a/packages/drizzle/CHANGELOG.md +++ b/packages/drizzle/CHANGELOG.md @@ -1,5 +1,12 @@ # @authhero/drizzle +## 0.1.64 + +### Patch Changes + +- Updated dependencies + - @authhero/adapter-interfaces@0.29.0 + ## 0.1.63 ### Patch Changes diff --git a/packages/drizzle/package.json b/packages/drizzle/package.json index 7bbfed1..048d739 100644 --- a/packages/drizzle/package.json +++ b/packages/drizzle/package.json @@ -11,7 +11,7 @@ "type": "git", "url": "https://github.com/markusahlstrand/authhero" }, - "version": "0.1.63", + "version": "0.1.64", "files": [ "dist" ], diff --git a/packages/kysely/CHANGELOG.md b/packages/kysely/CHANGELOG.md index ffbc3ec..1c0489a 100644 --- a/packages/kysely/CHANGELOG.md +++ b/packages/kysely/CHANGELOG.md @@ -1,5 +1,12 @@ # @authhero/kysely-adapter +## 0.24.2 + +### Patch Changes + +- Updated dependencies + - @authhero/adapter-interfaces@0.29.0 + ## 0.24.1 ### Patch Changes diff --git a/packages/kysely/package.json b/packages/kysely/package.json index 0ebb908..120530b 100644 --- a/packages/kysely/package.json +++ b/packages/kysely/package.json @@ -11,7 +11,7 @@ "type": "git", "url": "https://github.com/markusahlstrand/authhero" }, - "version": "0.24.1", + "version": "0.24.2", "files": [ "dist" ],