Skip to content

Commit d1794a1

Browse files
authored
Merge pull request #6 from probitas-test/fix/oidc
Add PKCE support and remove clientSecret from OIDC client
2 parents 210be72 + 79cfca5 commit d1794a1

File tree

1 file changed

+53
-17
lines changed
  • packages/probitas-client-http

1 file changed

+53
-17
lines changed

packages/probitas-client-http/oidc.ts

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
*
99
* - **OIDC Discovery**: Auto-discover endpoints from `/.well-known/openid-configuration` (RFC 8414)
1010
* - **Authorization Code Grant**: Automated browser-based login flow for testing
11+
* - **PKCE Support**: Automatic PKCE (S256) for enhanced security (RFC 7636)
12+
* - **Public Client Support**: Works with Public Clients (SPAs, Native Apps) - no client secret required
1113
* - **Token Management**: Automatic Authorization header injection after login
1214
* - **Type Safety**: Discriminated union for login params and results with type-safe error handling
1315
* - **Extensible**: Easy to add new grant types via discriminated union pattern
@@ -23,17 +25,16 @@
2325
* ```ts
2426
* import { createOidcHttpClient } from "@probitas/client-http/oidc";
2527
*
26-
* // Manual endpoint configuration
28+
* // OIDC Discovery (recommended)
2729
* const http = await createOidcHttpClient({
2830
* url: "http://localhost:3000",
2931
* oidc: {
30-
* authUrl: "/oauth/authorize",
31-
* tokenUrl: "/oauth/token",
32+
* issuer: "http://localhost:3000", // Auto-discover endpoints
3233
* clientId: "test-client",
3334
* },
3435
* });
3536
*
36-
* // Login with Authorization Code Grant
37+
* // Login with Authorization Code Grant (PKCE enabled automatically)
3738
* const result = await http.login({
3839
* type: "authorization_code",
3940
* username: "testuser",
@@ -94,10 +95,8 @@
9495
* await using http = await createOidcHttpClient({
9596
* url: "http://localhost:3000",
9697
* oidc: {
97-
* authUrl: "/oauth/authorize",
98-
* tokenUrl: "/oauth/token",
98+
* issuer: "http://localhost:3000",
9999
* clientId: "test-client",
100-
* clientSecret: "secret",
101100
* },
102101
* });
103102
*
@@ -113,6 +112,21 @@
113112
* // Automatic cleanup on scope exit
114113
* ```
115114
*
115+
* ## Manual Endpoint Configuration
116+
*
117+
* If your OIDC provider doesn't support Discovery, you can specify endpoints manually:
118+
*
119+
* ```ts
120+
* const http = await createOidcHttpClient({
121+
* url: "http://localhost:3000",
122+
* oidc: {
123+
* authUrl: "/oauth/authorize",
124+
* tokenUrl: "/oauth/token",
125+
* clientId: "test-client",
126+
* },
127+
* });
128+
* ```
129+
*
116130
* ## Related Packages
117131
*
118132
* | Package | Description |
@@ -143,7 +157,7 @@ import { createHttpClient } from "./client.ts";
143157
export interface OidcHttpClient extends HttpClient {
144158
/**
145159
* Perform OIDC login with specified grant type
146-
* Currently supports Authorization Code Grant with automatic form submission
160+
* Currently supports Authorization Code Grant with automatic form submission and PKCE (S256)
147161
*
148162
* @param params - Login parameters (discriminated union by type)
149163
* @returns Login result with token information
@@ -224,8 +238,6 @@ export interface OidcClientConfig {
224238
tokenUrl?: string;
225239
/** Client ID */
226240
clientId: string;
227-
/** Client secret (optional, for confidential clients) */
228-
clientSecret?: string;
229241
/** Redirect URI (default: "http://localhost/callback") */
230242
redirectUri?: string;
231243
/** OAuth scopes (default: "openid profile") */
@@ -360,8 +372,7 @@ async function fetchDiscoveryMetadata(
360372
* const http = await createOidcHttpClient({
361373
* url: "http://localhost:3000",
362374
* oidc: {
363-
* authUrl: "/oauth/authorize",
364-
* tokenUrl: "/oauth/token",
375+
* issuer: "http://localhost:3000",
365376
* clientId: "test-client",
366377
* },
367378
* });
@@ -542,24 +553,52 @@ export async function createOidcHttpClient(
542553
},
543554
};
544555

556+
// PKCE (Proof Key for Code Exchange) helpers (RFC 7636)
557+
function generateCodeVerifier(): string {
558+
// Generate 43-128 character random string using unreserved characters
559+
// Using base64url encoding of 32 random bytes = 43 characters
560+
const array = new Uint8Array(32);
561+
crypto.getRandomValues(array);
562+
return btoa(String.fromCharCode(...array))
563+
.replace(/\+/g, "-")
564+
.replace(/\//g, "_")
565+
.replace(/=/g, "");
566+
}
567+
568+
async function generateCodeChallenge(verifier: string): Promise<string> {
569+
// S256: BASE64URL(SHA256(ASCII(code_verifier)))
570+
const encoder = new TextEncoder();
571+
const data = encoder.encode(verifier);
572+
const hash = await crypto.subtle.digest("SHA-256", data);
573+
return btoa(String.fromCharCode(...new Uint8Array(hash)))
574+
.replace(/\+/g, "-")
575+
.replace(/\//g, "_")
576+
.replace(/=/g, "");
577+
}
578+
545579
// Authorization Code Grant implementation
546580
async function loginWithAuthorizationCodeGrant(
547581
username: string,
548582
password: string,
549583
options?: OidcAuthCodeOptions,
550584
): Promise<OidcLoginResult> {
551585
try {
552-
// Step 1: Generate state for CSRF protection (standard OIDC)
586+
// Step 1: Generate state and PKCE parameters
553587
const state = crypto.randomUUID();
588+
const codeVerifier = generateCodeVerifier();
589+
const codeChallenge = await generateCodeChallenge(codeVerifier);
554590

555591
// Get login form from authorization endpoint
556592
const loginFormUrl = options?.loginFormUrl || oidcConfig.authUrl;
557593
const loginFormResponse = await baseClient.get(loginFormUrl, {
558594
query: {
595+
client_id: oidcConfig.clientId,
559596
redirect_uri: oidcConfig.redirectUri,
560597
response_type: "code",
561598
scope: oidcConfig.scope,
562599
state, // Client-generated state
600+
code_challenge: codeChallenge, // PKCE
601+
code_challenge_method: "S256", // PKCE method
563602
},
564603
});
565604

@@ -657,12 +696,9 @@ export async function createOidcHttpClient(
657696
code,
658697
redirect_uri: oidcConfig.redirectUri,
659698
client_id: oidcConfig.clientId,
699+
code_verifier: codeVerifier, // PKCE
660700
});
661701

662-
if (oidcConfig.clientSecret) {
663-
tokenBody.set("client_secret", oidcConfig.clientSecret);
664-
}
665-
666702
const tokenResponse = await baseClient.post(oidcConfig.tokenUrl, {
667703
body: tokenBody,
668704
headers: {

0 commit comments

Comments
 (0)