mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-19 11:24:57 -05:00
refactor(oauth-provider)!: replace client type with application_type
Client registration carried two overlapping profile fields. `type` (web/native/user-agent-based) was a better-auth invention with no RFC 7591 standing, validated only for self-consistency against the auth method; `application_type` is the OIDC Registration field, and the one MCP clients are told to send. Registration accepted both without cross-checking, so a client could declare `type: "web"` alongside `application_type: "native"` and be stored claiming two profiles. Collapsing to `application_type` removes the ambiguity and gives the field a real column rather than the untyped metadata bag. `user-agent-based` has no successor: a browser app registers with `token_endpoint_auth_method: "none"`, which already marks it public. PKCE enforcement reads `application_type` for the native case that `type` used to cover, so a native client stays public whatever auth method it registered with.
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
---
|
||||
"@better-auth/oauth-provider": minor
|
||||
"@better-auth/cimd": minor
|
||||
---
|
||||
|
||||
OAuth clients describe their profile with one field, `application_type`, instead of two overlapping ones. The non-standard `type` field is removed, and its `web` and `native` values move to `application_type`; `user-agent-based` has no replacement, since a browser app registers with `token_endpoint_auth_method: "none"`, which already marks it public.
|
||||
|
||||
This release is breaking. Registration requests and the client management APIs no longer accept `type`, and client responses no longer return it. Replace `type: "web"` with `application_type: "web"`, `type: "native"` with `application_type: "native"`, and drop `type: "user-agent-based"`. A `native` client is now always treated as public, so PKCE is required for it even when the client registered with `require_pkce: false`.
|
||||
|
||||
The client table gains an `applicationType` column and no longer uses `type`. Run a migration after upgrading with `npx auth migrate` or `npx auth generate`. Existing clients keep working; those that had set `type` are treated as if no application type was declared, which leaves their redirect URIs unconstrained.
|
||||
@@ -147,7 +147,7 @@ await auth.api.adminCreateOAuthClient({
|
||||
headers,
|
||||
body: {
|
||||
token_endpoint_auth_method: "none",
|
||||
type: "native",
|
||||
application_type: "native",
|
||||
grant_types: [DEVICE_CODE_GRANT_TYPE, "refresh_token"],
|
||||
scope: "openid profile offline_access api:read",
|
||||
resources: ["https://api.example.com"],
|
||||
|
||||
@@ -448,7 +448,7 @@ const client = await authClient.oauth2.register({
|
||||
|
||||
For all endpoint parameters, see [RFC 7591 Registration](https://datatracker.ietf.org/doc/html/rfc7591#section-2).
|
||||
|
||||
When `application_type` ([OIDC Registration §2](https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata)) is provided, redirect URIs are constrained to what the declared type allows. A `native` client may use custom schemes, loopback `http` redirects, or claimed `https` links ([RFC 8252 §7](https://datatracker.ietf.org/doc/html/rfc8252#section-7)); plain `http` on a routable host is rejected. A `web` client requires `https` redirect URIs on non-loopback hosts. Sending both `application_type` and `type` requires them to agree. When `application_type` is omitted, no constraints are applied.
|
||||
When `application_type` ([OIDC Registration §2](https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata)) is provided, redirect URIs are constrained to what the declared type allows. A `native` client may use custom schemes, loopback `http` redirects, or claimed `https` links ([RFC 8252 §7](https://datatracker.ietf.org/doc/html/rfc8252#section-7)); plain `http` on a routable host is rejected. A `web` client requires `https` redirect URIs on non-loopback hosts, and is the only type that may authenticate at the token endpoint with a secret; a `native` client is always treated as public, so PKCE is required for it. When `application_type` is omitted, no constraints are applied.
|
||||
|
||||
The [MCP 2026-07-28 spec](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/client-registration#application-type-and-redirect-uri-constraints) requires MCP clients to send an appropriate `application_type`. The same constraints apply to clients identified by a [Client ID Metadata Document](/docs/plugins/cimd).
|
||||
|
||||
@@ -2005,10 +2005,10 @@ export const oauthClientTableFields = [
|
||||
isOptional: true,
|
||||
},
|
||||
{
|
||||
name: "type",
|
||||
name: "application_type",
|
||||
type: "string",
|
||||
description:
|
||||
"Type of OAuth client. Supports: ['web', 'native', 'user-agent-based']",
|
||||
"OIDC Registration application type. Supports: ['web', 'native']. Native clients are always treated as public.",
|
||||
isOptional: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -99,7 +99,6 @@ const ALLOWED_METADATA_FIELDS = new Set([
|
||||
"post_logout_redirect_uris",
|
||||
"subject_type",
|
||||
"application_type",
|
||||
"type",
|
||||
"jwks",
|
||||
"jwks_uri",
|
||||
]);
|
||||
|
||||
@@ -61,7 +61,6 @@ describe("oauth-provider device-code grant", async () => {
|
||||
token_endpoint_auth_method: "none",
|
||||
grant_types: grantTypes,
|
||||
scope: "openid profile email",
|
||||
type: "native",
|
||||
},
|
||||
});
|
||||
return created!.client_id;
|
||||
@@ -353,7 +352,6 @@ describe("oauth-provider device-code grant", async () => {
|
||||
token_endpoint_auth_method: "none",
|
||||
grant_types: [DEVICE_CODE_GRANT_TYPE],
|
||||
scope: "openid",
|
||||
type: "native",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -380,7 +378,6 @@ describe("oauth-provider device-code grant", async () => {
|
||||
token_endpoint_auth_method: "client_secret_basic",
|
||||
grant_types: [DEVICE_CODE_GRANT_TYPE],
|
||||
scope: "openid",
|
||||
type: "web",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -480,7 +477,6 @@ describe("oauth-provider device-code grant expiry", async () => {
|
||||
token_endpoint_auth_method: "none",
|
||||
grant_types: [DEVICE_CODE_GRANT_TYPE],
|
||||
scope: "openid",
|
||||
type: "native",
|
||||
},
|
||||
});
|
||||
const clientId = created!.client_id;
|
||||
|
||||
@@ -273,7 +273,6 @@ describe("oauth-provider extensions", async () => {
|
||||
token_endpoint_auth_method: extensionAuthMethod,
|
||||
grant_types: [extensionGrant],
|
||||
scope: "openid email vc",
|
||||
type: "web",
|
||||
},
|
||||
});
|
||||
expect(adminClient?.client_id).toBeDefined();
|
||||
@@ -289,7 +288,6 @@ describe("oauth-provider extensions", async () => {
|
||||
token_endpoint_auth_method: extensionAuthMethod,
|
||||
grant_types: [extensionGrant],
|
||||
scope: "openid email vc",
|
||||
type: "web",
|
||||
},
|
||||
});
|
||||
expect(registeredClient?.client_id).toBeDefined();
|
||||
@@ -311,7 +309,6 @@ describe("oauth-provider extensions", async () => {
|
||||
grant_types: [extensionGrant],
|
||||
response_types: ["code"],
|
||||
scope: "openid email vc",
|
||||
type: "web",
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -336,7 +333,6 @@ describe("oauth-provider extensions", async () => {
|
||||
grant_types: [extensionGrant],
|
||||
jwks_uri: "https://127.0.0.1/jwks",
|
||||
scope: "openid email vc",
|
||||
type: "web",
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -362,7 +358,6 @@ describe("oauth-provider extensions", async () => {
|
||||
jwks: [{ kty: "RSA", n: "test", e: "test-exponent" }],
|
||||
jwks_uri: "https://client.example.com/jwks",
|
||||
scope: "openid email vc",
|
||||
type: "web",
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -397,7 +392,6 @@ describe("oauth-provider extensions", async () => {
|
||||
body: {
|
||||
grant_types: [extensionGrant],
|
||||
scope: "openid email vc",
|
||||
type: "web",
|
||||
},
|
||||
});
|
||||
const response = await client.$fetch("/oauth2/token", {
|
||||
@@ -496,7 +490,6 @@ describe("oauth-provider extensions", async () => {
|
||||
token_endpoint_auth_method: extensionAuthMethod,
|
||||
grant_types: [extensionGrant],
|
||||
scope: "openid email vc",
|
||||
type: "web",
|
||||
},
|
||||
});
|
||||
expect(oauthClient?.client_id).toBeDefined();
|
||||
@@ -608,7 +601,6 @@ describe("oauth-provider extensions", async () => {
|
||||
token_endpoint_auth_method: extensionAuthMethod,
|
||||
grant_types: [extensionOpaqueGrant],
|
||||
scope: "openid email vc",
|
||||
type: "web",
|
||||
},
|
||||
});
|
||||
const response = await client.$fetch("/oauth2/token", {
|
||||
@@ -975,7 +967,6 @@ describe("oauth-provider extensions", async () => {
|
||||
token_endpoint_auth_method: extensionAuthMethod,
|
||||
grant_types: [extensionOpaqueGrant],
|
||||
scope: "openid email vc",
|
||||
type: "web",
|
||||
},
|
||||
});
|
||||
const tokenResponse = await client.$fetch<{ access_token: string }>(
|
||||
@@ -1025,7 +1016,6 @@ describe("oauth-provider extensions", async () => {
|
||||
token_endpoint_auth_method: extensionAuthMethod,
|
||||
grant_types: [extensionGrant],
|
||||
scope: "openid email vc",
|
||||
type: "web",
|
||||
},
|
||||
});
|
||||
const response = await client.$fetch("/oauth2/token", {
|
||||
@@ -1052,7 +1042,6 @@ describe("oauth-provider extensions", async () => {
|
||||
token_endpoint_auth_method: extensionAuthMethod,
|
||||
grant_types: [extensionOpaqueGrant],
|
||||
scope: "openid email vc",
|
||||
type: "web",
|
||||
},
|
||||
});
|
||||
const tokenResponse = await client.$fetch<{ access_token: string }>(
|
||||
@@ -1134,7 +1123,6 @@ describe("oauth-provider extensions", async () => {
|
||||
token_endpoint_auth_method: extensionAuthMethod,
|
||||
grant_types: [extensionOpaqueBoundGrant],
|
||||
scope: "openid email vc",
|
||||
type: "web",
|
||||
},
|
||||
});
|
||||
const clientAuth = {
|
||||
|
||||
@@ -1598,12 +1598,12 @@ export const oauthProvider = <O extends OAuthOptions<Scope[]>>(options: O) => {
|
||||
public: {
|
||||
type: "boolean",
|
||||
description:
|
||||
"Whether the client is public as determined by the type",
|
||||
"Whether the client is a public client",
|
||||
},
|
||||
type: {
|
||||
application_type: {
|
||||
type: "string",
|
||||
description: "Type of the client",
|
||||
enum: ["web", "native", "user-agent-based"],
|
||||
description: "OIDC Registration application type",
|
||||
enum: ["web", "native"],
|
||||
},
|
||||
disabled: {
|
||||
type: "boolean",
|
||||
|
||||
@@ -48,7 +48,7 @@ export const adminCreateOAuthClient = (opts: OAuthOptions<Scope[]>) =>
|
||||
jwks_uri: z.string().optional(),
|
||||
grant_types: grantTypesSchema.optional(),
|
||||
response_types: z.array(z.enum(["code"])).optional(),
|
||||
type: z.enum(["web", "native", "user-agent-based"]).optional(),
|
||||
application_type: z.enum(["web", "native"]).optional(),
|
||||
// SERVER_ONLY applicable fields
|
||||
client_secret_expires_at: z
|
||||
.union([z.string(), z.number()])
|
||||
@@ -177,13 +177,12 @@ export const adminCreateOAuthClient = (opts: OAuthOptions<Scope[]>) =>
|
||||
},
|
||||
public: {
|
||||
type: "boolean",
|
||||
description:
|
||||
"Whether the client is public as determined by the type",
|
||||
description: "Whether the client is a public client",
|
||||
},
|
||||
type: {
|
||||
application_type: {
|
||||
type: "string",
|
||||
description: "Type of the client",
|
||||
enum: ["web", "native", "user-agent-based"],
|
||||
description: "OIDC Registration application type",
|
||||
enum: ["web", "native"],
|
||||
},
|
||||
disabled: {
|
||||
type: "boolean",
|
||||
@@ -250,7 +249,7 @@ export const createOAuthClient = (opts: OAuthOptions<Scope[]>) =>
|
||||
jwks_uri: z.string().optional(),
|
||||
grant_types: grantTypesSchema.optional(),
|
||||
response_types: z.array(z.enum(["code"])).optional(),
|
||||
type: z.enum(["web", "native", "user-agent-based"]).optional(),
|
||||
application_type: z.enum(["web", "native"]).optional(),
|
||||
// RFC 9449 §5.2: client asks for DPoP-bound access tokens.
|
||||
dpop_bound_access_tokens: z.boolean().optional(),
|
||||
}),
|
||||
@@ -368,13 +367,12 @@ export const createOAuthClient = (opts: OAuthOptions<Scope[]>) =>
|
||||
},
|
||||
public: {
|
||||
type: "boolean",
|
||||
description:
|
||||
"Whether the client is public as determined by the type",
|
||||
description: "Whether the client is a public client",
|
||||
},
|
||||
type: {
|
||||
application_type: {
|
||||
type: "string",
|
||||
description: "Type of the client",
|
||||
enum: ["web", "native", "user-agent-based"],
|
||||
description: "OIDC Registration application type",
|
||||
enum: ["web", "native"],
|
||||
},
|
||||
disabled: {
|
||||
type: "boolean",
|
||||
@@ -509,7 +507,7 @@ export const adminUpdateOAuthClient = (opts: OAuthOptions<Scope[]>) =>
|
||||
// NOTE: token_endpoint_auth_method is currently immutable since it changes isPublic definition
|
||||
grant_types: grantTypesSchema.optional(),
|
||||
response_types: z.array(z.enum(["code"])).optional(),
|
||||
type: z.enum(["web", "native", "user-agent-based"]).optional(),
|
||||
application_type: z.enum(["web", "native"]).optional(),
|
||||
// SERVER_ONLY applicable fields
|
||||
client_secret_expires_at: z
|
||||
.union([z.string(), z.number()])
|
||||
@@ -559,7 +557,7 @@ export const updateOAuthClient = (opts: OAuthOptions<Scope[]>) =>
|
||||
// NOTE: token_endpoint_auth_method is currently immutable since it changes isPublic definition
|
||||
grant_types: grantTypesSchema.optional(),
|
||||
response_types: z.array(z.enum(["code"])).optional(),
|
||||
type: z.enum(["web", "native", "user-agent-based"]).optional(),
|
||||
application_type: z.enum(["web", "native"]).optional(),
|
||||
}),
|
||||
}),
|
||||
metadata: {
|
||||
|
||||
@@ -110,37 +110,12 @@ describe("oauth register", async () => {
|
||||
expect(response.error?.status).toBe(400);
|
||||
});
|
||||
|
||||
it("should fail type check for public client request", async () => {
|
||||
const response = await serverClient.oauth2.register({
|
||||
token_endpoint_auth_method: "none",
|
||||
type: "web",
|
||||
redirect_uris: [redirectUri],
|
||||
});
|
||||
expect(response.error?.status).toBe(400);
|
||||
});
|
||||
|
||||
it.for([
|
||||
"native",
|
||||
"user-agent-based",
|
||||
] as OAuthClient["type"][])("should fail with type '%s' check for confidential client request", async (type) => {
|
||||
const response = await serverClient.oauth2.register({
|
||||
token_endpoint_auth_method: "client_secret_post",
|
||||
type,
|
||||
redirect_uris: [redirectUri],
|
||||
});
|
||||
expect(response.error?.status).toBe(400);
|
||||
});
|
||||
|
||||
it.for([
|
||||
"native",
|
||||
"user-agent-based",
|
||||
] as OAuthClient["type"][])("should register public '%s' client with minimum requirements via server", async (type) => {
|
||||
it("should register public client with minimum requirements via server", async () => {
|
||||
const response = await auth.api.adminCreateOAuthClient({
|
||||
headers,
|
||||
body: {
|
||||
token_endpoint_auth_method: "none",
|
||||
redirect_uris: [redirectUri],
|
||||
type,
|
||||
},
|
||||
});
|
||||
expect(response?.client_id).toBeDefined();
|
||||
@@ -182,7 +157,6 @@ describe("oauth register", async () => {
|
||||
response_types: ["code"],
|
||||
//---- RFC6749 Spec ----//
|
||||
public: true, // test never set on this (based off of token_endpoint_auth_method)
|
||||
type: "web",
|
||||
//---- Not Part of RFC7591 Spec ----//
|
||||
disabled: false,
|
||||
};
|
||||
@@ -234,10 +208,9 @@ describe("oauth register", async () => {
|
||||
expect(response.data?.disabled).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should preserve confidential method and type for authenticated registration", async () => {
|
||||
it("should preserve the confidential auth method for authenticated registration", async () => {
|
||||
const response = await serverClient.oauth2.register({
|
||||
token_endpoint_auth_method: "client_secret_post",
|
||||
type: "web",
|
||||
redirect_uris: [redirectUri],
|
||||
});
|
||||
expect(response.data?.client_id).toBeDefined();
|
||||
@@ -245,7 +218,6 @@ describe("oauth register", async () => {
|
||||
expect(response.data?.token_endpoint_auth_method).toBe(
|
||||
"client_secret_post",
|
||||
);
|
||||
expect(response.data?.type).toBe("web");
|
||||
expect(response.data?.public).toBeFalsy();
|
||||
});
|
||||
|
||||
@@ -480,7 +452,6 @@ describe("oauth register", async () => {
|
||||
const response = await serverClient.oauth2.register({
|
||||
redirect_uris: [redirectUri],
|
||||
token_endpoint_auth_method: "none",
|
||||
type: "native",
|
||||
backchannel_logout_uri: `${rpBaseUrl}/logout/backchannel`,
|
||||
});
|
||||
expect(response.data?.client_id).toBeDefined();
|
||||
@@ -645,10 +616,9 @@ describe("oauth register - unauthenticated", async () => {
|
||||
/**
|
||||
* @see https://github.com/better-auth/better-auth/issues/8588
|
||||
*/
|
||||
it("should preserve type 'web' for unauthenticated confidential DCR", async () => {
|
||||
it("should keep unauthenticated confidential DCR confidential", async () => {
|
||||
const response = await unauthenticatedClient.oauth2.register({
|
||||
token_endpoint_auth_method: "client_secret_post",
|
||||
type: "web",
|
||||
redirect_uris: [redirectUri],
|
||||
});
|
||||
expect(response.data?.client_id).toBeDefined();
|
||||
@@ -656,7 +626,7 @@ describe("oauth register - unauthenticated", async () => {
|
||||
expect(response.data?.token_endpoint_auth_method).toBe(
|
||||
"client_secret_post",
|
||||
);
|
||||
expect(response.data?.type).toBe("web");
|
||||
expect(response.data?.public).toBeFalsy();
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1427,7 +1397,7 @@ describe("oauth register - protected dynamic registration", async () => {
|
||||
|
||||
describe("oauth register - application_type", async () => {
|
||||
const authServerBaseUrl = "http://localhost:3000";
|
||||
const { customFetchImpl } = await getTestInstance({
|
||||
const { auth, signInWithTestUser, customFetchImpl } = await getTestInstance({
|
||||
baseURL: authServerBaseUrl,
|
||||
plugins: [
|
||||
jwt(),
|
||||
@@ -1507,25 +1477,42 @@ describe("oauth register - application_type", async () => {
|
||||
expect(body.application_type).toBe("web");
|
||||
});
|
||||
|
||||
it("rejects an application_type that contradicts type", async () => {
|
||||
const response = await register({
|
||||
type: "web",
|
||||
application_type: "native",
|
||||
redirect_uris: ["https://rp.example.com/callback"],
|
||||
});
|
||||
expect(response.status).toBe(400);
|
||||
const body = (await response.json()) as { error_description?: string };
|
||||
expect(body.error_description).toContain("conflicts with type");
|
||||
});
|
||||
it("forces PKCE on a native client even when the client opts out", async () => {
|
||||
// A native client cannot keep a secret, so it is public whatever auth
|
||||
// method it registered with, and `require_pkce: false` cannot waive PKCE
|
||||
// for it. The same client declared `web` is taken at its word.
|
||||
const { headers } = await signInWithTestUser();
|
||||
const authorizeWithoutPkce = async (
|
||||
applicationType: "web" | "native",
|
||||
redirectUri: string,
|
||||
) => {
|
||||
const created = await auth.api.adminCreateOAuthClient({
|
||||
headers,
|
||||
body: {
|
||||
application_type: applicationType,
|
||||
token_endpoint_auth_method: "client_secret_post",
|
||||
require_pkce: false,
|
||||
redirect_uris: [redirectUri],
|
||||
},
|
||||
});
|
||||
const response = await customFetchImpl(
|
||||
`${authServerBaseUrl}/api/auth/oauth2/authorize?${new URLSearchParams({
|
||||
client_id: created!.client_id!,
|
||||
response_type: "code",
|
||||
redirect_uri: redirectUri,
|
||||
scope: "openid",
|
||||
})}`,
|
||||
{ method: "GET", redirect: "manual" },
|
||||
);
|
||||
return response.headers.get("location") ?? "";
|
||||
};
|
||||
|
||||
it("accepts a user-agent-based client declaring the web application type", async () => {
|
||||
const response = await register({
|
||||
type: "user-agent-based",
|
||||
application_type: "web",
|
||||
token_endpoint_auth_method: "none",
|
||||
redirect_uris: ["https://spa.example.com/callback"],
|
||||
});
|
||||
expect(response.status).toBe(201);
|
||||
expect(
|
||||
await authorizeWithoutPkce("native", "com.example.pkce:/cb"),
|
||||
).toContain("pkce+is+required");
|
||||
expect(
|
||||
await authorizeWithoutPkce("web", "https://rp.example.com/pkce-cb"),
|
||||
).not.toContain("pkce+is+required");
|
||||
});
|
||||
|
||||
it("keeps prior behavior when application_type is omitted", async () => {
|
||||
|
||||
@@ -258,27 +258,6 @@ export async function checkOAuthClient(
|
||||
});
|
||||
}
|
||||
|
||||
// Check value of type, if sent, matches isPublic
|
||||
if (clientWithDefaults.type) {
|
||||
if (
|
||||
isPublic &&
|
||||
!(
|
||||
clientWithDefaults.type === "native" ||
|
||||
clientWithDefaults.type === "user-agent-based"
|
||||
)
|
||||
) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
error: "invalid_client_metadata",
|
||||
error_description: `Type must be 'native' or 'user-agent-based' for public applications`,
|
||||
});
|
||||
} else if (!isPublic && !(clientWithDefaults.type === "web")) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
error: "invalid_client_metadata",
|
||||
error_description: `Type must be 'web' for confidential applications`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const grantTypes = clientWithDefaults.grant_types ?? [];
|
||||
const responseTypes = clientWithDefaults.response_types;
|
||||
|
||||
@@ -295,19 +274,6 @@ export async function checkOAuthClient(
|
||||
});
|
||||
}
|
||||
|
||||
// `type` and `application_type` both describe the client profile, so a
|
||||
// registration that sends both must not contradict itself.
|
||||
if (clientWithDefaults.type && clientWithDefaults.application_type) {
|
||||
const impliedApplicationType =
|
||||
clientWithDefaults.type === "native" ? "native" : "web";
|
||||
if (impliedApplicationType !== clientWithDefaults.application_type) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
error: "invalid_client_metadata",
|
||||
error_description: `application_type '${clientWithDefaults.application_type}' conflicts with type '${clientWithDefaults.type}'`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// OIDC Registration §2 `application_type`, required of MCP clients by
|
||||
// SEP-837. Native redirect URIs follow RFC 8252 §7 (custom scheme, loopback
|
||||
// http, or claimed https link) rather than OIDC's narrower list, which
|
||||
@@ -765,7 +731,7 @@ export function oauthToSchema(input: OAuthClient): SchemaClient<Scope[]> {
|
||||
response_types: responseTypes,
|
||||
// RFC6749 Spec
|
||||
public: _public,
|
||||
type,
|
||||
application_type: applicationType,
|
||||
// Not Part of RFC7591 Spec
|
||||
disabled,
|
||||
skip_consent: skipConsent,
|
||||
@@ -833,7 +799,7 @@ export function oauthToSchema(input: OAuthClient): SchemaClient<Scope[]> {
|
||||
jwksUri: jwksUri,
|
||||
// RFC6749 Spec
|
||||
public: _public,
|
||||
type,
|
||||
applicationType,
|
||||
// All other metadata
|
||||
skipConsent,
|
||||
enableEndSession,
|
||||
@@ -884,7 +850,7 @@ export function schemaToOAuth(input: SchemaClient<Scope[]>): OAuthClient {
|
||||
responseTypes,
|
||||
// RFC6749 Spec
|
||||
public: _public,
|
||||
type,
|
||||
applicationType,
|
||||
// Jwks
|
||||
jwks,
|
||||
jwksUri,
|
||||
@@ -946,7 +912,7 @@ export function schemaToOAuth(input: SchemaClient<Scope[]>): OAuthClient {
|
||||
response_types: responseTypes ?? undefined,
|
||||
// RFC6749 Spec
|
||||
public: _public ?? undefined,
|
||||
type: type ?? undefined,
|
||||
application_type: applicationType ?? undefined,
|
||||
// Not Part of RFC7591 Spec
|
||||
disabled: disabled ?? undefined,
|
||||
skip_consent: skipConsent ?? undefined,
|
||||
|
||||
@@ -133,7 +133,7 @@ export const schema = {
|
||||
type: "boolean",
|
||||
required: false,
|
||||
},
|
||||
type: {
|
||||
applicationType: {
|
||||
type: "string",
|
||||
required: false,
|
||||
},
|
||||
|
||||
@@ -1712,15 +1712,14 @@ export interface SchemaClient<
|
||||
*/
|
||||
public?: boolean;
|
||||
/**
|
||||
* The client type
|
||||
* OIDC Registration application type.
|
||||
*
|
||||
* Described https://www.rfc-editor.org/rfc/rfc6749.html#section-2.1
|
||||
* Constrains which redirect URIs the client may register, and marks
|
||||
* `native` clients as public so PKCE is always required for them.
|
||||
*
|
||||
* - web - A web application (confidential client)
|
||||
* - native - A mobile application (public client)
|
||||
* - user-agent-based - A user-agent-based application (public client)
|
||||
* @see https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata
|
||||
*/
|
||||
type?: "web" | "native" | "user-agent-based";
|
||||
applicationType?: "web" | "native";
|
||||
/**
|
||||
* Whether this client requires PKCE for authorization code flow.
|
||||
*
|
||||
|
||||
@@ -399,7 +399,6 @@ export interface OAuthClient {
|
||||
application_type?: "web" | "native";
|
||||
//---- RFC6749 Spec ----//
|
||||
public?: boolean;
|
||||
type?: "web" | "native" | "user-agent-based";
|
||||
//---- Not Part of RFC7591 Spec ----//
|
||||
disabled?: boolean;
|
||||
skip_consent?: boolean;
|
||||
|
||||
@@ -206,9 +206,7 @@ export const clientRegistrationRequestSchema = z.object({
|
||||
jwks_uri: z.string().optional(),
|
||||
grant_types: z.array(z.string().trim().min(1)).min(1).optional(),
|
||||
response_types: z.array(z.enum(["code"])).optional(),
|
||||
type: z.enum(["web", "native", "user-agent-based"]).optional(),
|
||||
// OIDC Registration §2: constrains redirect URI schemes and hosts when
|
||||
// provided, and must agree with `type` when both are sent.
|
||||
// OIDC Registration §2: constrains redirect URI schemes and hosts.
|
||||
application_type: z.enum(["web", "native"]).optional(),
|
||||
subject_type: z.enum(["public", "pairwise"]).optional(),
|
||||
// RFC 9449 §5.2: client asks for DPoP-bound access tokens.
|
||||
|
||||
@@ -956,8 +956,7 @@ export function isPKCERequired(
|
||||
// Determine if client is public
|
||||
const isPublicClient =
|
||||
client.tokenEndpointAuthMethod === "none" ||
|
||||
client.type === "native" ||
|
||||
client.type === "user-agent-based" ||
|
||||
client.applicationType === "native" ||
|
||||
client.public === true;
|
||||
|
||||
// PKCE always required for public clients
|
||||
|
||||
Reference in New Issue
Block a user