From b87f67d1eeb554fe6ff9f85eda46dd6a8f2717ec Mon Sep 17 00:00:00 2001 From: ZenDrx Date: Sat, 1 Aug 2026 08:39:53 +0100 Subject: [PATCH 1/8] Implement Argon2 hashing and verification functions --- src/pswd/argon2.zig | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/pswd/argon2.zig diff --git a/src/pswd/argon2.zig b/src/pswd/argon2.zig new file mode 100644 index 0000000..b7cb16c --- /dev/null +++ b/src/pswd/argon2.zig @@ -0,0 +1,32 @@ +const std = @import("std"); + +pub const Options = struct { + t: u32, + m: u32, + p: u24 +}; + +pub fn hash(password: []const u8, allocator: std.mem.Allocator, io: std.Io, options: Options) ![]const u8 { + var hash_buffer: [256]u8 = undefined; + const hash = try std.crypto.pwhash.argon2.strHash(password, .{ + .allocator = allocator, + .params = .{ + .t = options.t, + .m = options.m, + .p = options.p + }}, + &hash_buffer, + io + ); + return try allocator.dupe(u8, hash); +} + +pub fn verify(hash: []const u8, password: []const u8, alloc: std.mem.Allocator, io: std.Io) !Bool { + std.crypto.pwhash.argon2.strVerify(hash, password, .{ .allocator = alloc }, io) catch |err| { + if (err == error.AuthenticationFailed) { + return false; + } + return err; + }; +return true; +} From 315c7b9dcd053a2a4d2bf99965933b506e070998 Mon Sep 17 00:00:00 2001 From: ZenDrx Date: Sat, 1 Aug 2026 09:08:12 +0100 Subject: [PATCH 2/8] Implement bcrypt hashing and verification functions --- src/pswd/bcrypt.zig | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/pswd/bcrypt.zig diff --git a/src/pswd/bcrypt.zig b/src/pswd/bcrypt.zig new file mode 100644 index 0000000..c35d93c --- /dev/null +++ b/src/pswd/bcrypt.zig @@ -0,0 +1,27 @@ +const std = @import("std"); + +pub fn hash(password: []const u8, alloc: std.mem.Allocator, io: std.Io, rounds: u6) ![]const u8 { + var buffer: [256]u8 = undefined; + const hash = std.crypto.pwhash.bcrypt.strHash(password, .{ + .allocator = alloc, + .params = .{ + .rounds_log = rounds, + .silently_truncate_password = false, + }, + .encoding = crypt, + }, + &buffer, + io + ); + return try alloc.dupe(u8, hash); +} + +pub fn verify(password: []const u8, hash: []const u8, alloc: std.mem.Allocator) !bool { + std.crypto.pwhash.bcrypt.strVerify(hash, password, .{ .allocator = alloc, .silently_truncate_password = false }) catch |err| { + if (err == error.AuthenticationFailed) { + return false; + } + return err; +}; +return true; +} From da87506ff91fb787e4ec125bc0047039e52d10f8 Mon Sep 17 00:00:00 2001 From: ZenDrx Date: Sat, 1 Aug 2026 09:09:01 +0100 Subject: [PATCH 3/8] Fix return type of verify function to bool --- src/pswd/argon2.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pswd/argon2.zig b/src/pswd/argon2.zig index b7cb16c..b7690b8 100644 --- a/src/pswd/argon2.zig +++ b/src/pswd/argon2.zig @@ -21,7 +21,7 @@ pub fn hash(password: []const u8, allocator: std.mem.Allocator, io: std.Io, opti return try allocator.dupe(u8, hash); } -pub fn verify(hash: []const u8, password: []const u8, alloc: std.mem.Allocator, io: std.Io) !Bool { +pub fn verify(hash: []const u8, password: []const u8, alloc: std.mem.Allocator, io: std.Io) !bool { std.crypto.pwhash.argon2.strVerify(hash, password, .{ .allocator = alloc }, io) catch |err| { if (err == error.AuthenticationFailed) { return false; From 5cc9469dfef3046800ae7cf896ac6d8fd15cd696 Mon Sep 17 00:00:00 2001 From: ZenDrx Date: Sun, 2 Aug 2026 06:38:23 +0100 Subject: [PATCH 4/8] Add password hashing implementations for argon2 and bcrypt --- src/pswd/pswd.zig | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/pswd/pswd.zig diff --git a/src/pswd/pswd.zig b/src/pswd/pswd.zig new file mode 100644 index 0000000..b730c84 --- /dev/null +++ b/src/pswd/pswd.zig @@ -0,0 +1,5 @@ +const argon2_impl = @import("argon2.zig"); +const bcrypt_impl = @import("bcrypt.zig"); + +pub const argon2 = argon2_impl; +pub const bcrypt = argon2_impl; From 2e4b3ce6add6d17bf5f5187acca71d06aef1b436 Mon Sep 17 00:00:00 2001 From: ZenDrx Date: Sun, 2 Aug 2026 07:06:33 +0100 Subject: [PATCH 5/8] Implement GitHub OAuth functionality in Zig --- src/oauth/github.zig | 148 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 src/oauth/github.zig diff --git a/src/oauth/github.zig b/src/oauth/github.zig new file mode 100644 index 0000000..657876c --- /dev/null +++ b/src/oauth/github.zig @@ -0,0 +1,148 @@ +const std = @import("std"); + +pub const GitHubOAuth = struct { + allocator: std.mem.Allocator, + io: std.Io, + client_id: []const u8, + client_secret: []const u8, + redirect_uri: []const u8, + + pub const User = struct { + id: u64, + login: []const u8, + name: ?[]const u8, + email: ?[]const u8, + avatar_url: ?[]const u8, + }; + + pub const TokenResponse = struct { + access_token: []const u8, + token_type: []const u8, + scope: []const u8, + }; + + pub fn init( + allocator: std.mem.Allocator, + io: std.Io, + client_id: []const u8, + client_secret: []const u8, + redirect_uri: []const u8, + ) GitHubOAuth { + return GitHubOAuth{ + .allocator = allocator, + .io = io, + .client_id = client_id, + .client_secret = client_secret, + .redirect_uri = redirect_uri, + }; + } + + pub fn deinit(self: *GitHubOAuth) void { + _ = self; + // User manages their own strings + } + + pub fn authorizationUrl(self: *GitHubOAuth, state: []const u8) ![]const u8 { + return try std.fmt.allocPrint(self.allocator, + "https://github.com/login/oauth/authorize?client_id={s}&redirect_uri={s}&state={s}", + .{ self.client_id, self.redirect_uri, state } + ); + } + + pub fn exchangeCode(self: *GitHubOAuth, code: []const u8) !TokenResponse { + var client = std.http.Client{ + .allocator = self.allocator, + .io = self.io, + }; + defer client.deinit(); + + var response_body = std.ArrayList(u8).empty; + defer response_body.deinit(self.allocator); + + const body = try std.fmt.allocPrint(self.allocator, + "client_id={s}&client_secret={s}&code={s}", + .{ self.client_id, self.client_secret, code } + ); + defer self.allocator.free(body); + + const result = try client.fetch(.{ + .location = .{ .url = "https://github.com/login/oauth/access_token" }, + .method = .POST, + .headers = .{ + .extra_headers = &.{ + .{ .name = "Accept", .value = "application/json" }, + .{ .name = "Content-Type", .value = "application/x-www-form-urlencoded" }, + }, + }, + .payload = body, + .response_writer = &response_body.writer(), + }); + + if (result.status.class() != .success) { + return error.GitHubOAuthFailed; + } + + const parsed = try std.json.parseFromSlice(TokenResponse, self.allocator, response_body.items, .{}); + defer parsed.deinit(); + + return TokenResponse{ + .access_token = try self.allocator.dupe(u8, parsed.value.access_token), + .token_type = try self.allocator.dupe(u8, parsed.value.token_type), + .scope = try self.allocator.dupe(u8, parsed.value.scope), + }; + } + + pub fn freeTokenResponse(self: *GitHubOAuth, token: *TokenResponse) void { + self.allocator.free(@constCast(token.access_token)); + self.allocator.free(@constCast(token.token_type)); + self.allocator.free(@constCast(token.scope)); + } + + pub fn fetchUser(self: *GitHubOAuth, access_token: []const u8) !User { + var client = std.http.Client{ + .allocator = self.allocator, + .io = self.io, + }; + defer client.deinit(); + + var response_body = std.ArrayList(u8).empty; + defer response_body.deinit(self.allocator); + + const auth_header = try std.fmt.allocPrint(self.allocator, "Bearer {s}", .{access_token}); + defer self.allocator.free(auth_header); + + const result = try client.fetch(.{ + .location = .{ .url = "https://api.github.com/user" }, + .method = .GET, + .headers = .{ + .extra_headers = &.{ + .{ .name = "Authorization", .value = auth_header }, + .{ .name = "Accept", .value = "application/json" }, + }, + }, + .response_writer = &response_body.writer(), + }); + + if (result.status.class() != .success) { + return error.GitHubUserFetchFailed; + } + + const parsed = try std.json.parseFromSlice(User, self.allocator, response_body.items, .{}); + defer parsed.deinit(); + + return User{ + .id = parsed.value.id, + .login = try self.allocator.dupe(u8, parsed.value.login), + .name = if (parsed.value.name) |n| try self.allocator.dupe(u8, n) else null, + .email = if (parsed.value.email) |e| try self.allocator.dupe(u8, e) else null, + .avatar_url = if (parsed.value.avatar_url) |a| try self.allocator.dupe(u8, a) else null, + }; + } + + pub fn freeUser(self: *GitHubOAuth, user: *User) void { + self.allocator.free(@constCast(user.login)); + if (user.name) |n| self.allocator.free(@constCast(n)); + if (user.email) |e| self.allocator.free(@constCast(e)); + if (user.avatar_url) |a| self.allocator.free(@constCast(a)); + } +}; From cb28890e9e453fd53c7ae461b6af53f6a01ebd5b Mon Sep 17 00:00:00 2001 From: ZenDrx Date: Sun, 2 Aug 2026 07:09:54 +0100 Subject: [PATCH 6/8] Implement Google OAuth functionality in Zig --- src/oauth/google.zig | 217 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 src/oauth/google.zig diff --git a/src/oauth/google.zig b/src/oauth/google.zig new file mode 100644 index 0000000..29da0f0 --- /dev/null +++ b/src/oauth/google.zig @@ -0,0 +1,217 @@ +const std = @import("std"); + +pub const GoogleOAuth = struct { + allocator: std.mem.Allocator, + io: std.Io, + client_id: []const u8, + client_secret: []const u8, + redirect_uri: []const u8, + + pub const User = struct { + id: []const u8, + email: []const u8, + name: []const u8, + given_name: []const u8, + family_name: []const u8, + picture: ?[]const u8, + }; + + pub const TokenResponse = struct { + access_token: []const u8, + refresh_token: ?[]const u8, + expires_in: u64, + token_type: []const u8, + scope: []const u8, + id_token: ?[]const u8, + }; + + pub fn init( + allocator: std.mem.Allocator, + io: std.Io, + client_id: []const u8, + client_secret: []const u8, + redirect_uri: []const u8, + ) GoogleOAuth { + return .{ + .allocator = allocator, + .io = io, + .client_id = client_id, + .client_secret = client_secret, + .redirect_uri = redirect_uri, + }; + } + + pub fn deinit(self: *GoogleOAuth) void { + _ = self; + } + + /// Generates the authorization URL with PKCE. + /// Returns the URL, code_verifier, and code_challenge. + pub fn authorizationUrl(self: *GoogleOAuth, state: []const u8) !struct { url: []u8, code_verifier: []u8, code_challenge: []u8 } { + // 1. Generate a random code verifier. + const verifier = try generateCodeVerifier(self.allocator); + errdefer self.allocator.free(verifier); + + // 2. Calculate the code challenge (SHA256 hash of the verifier). + const challenge = try generateCodeChallenge(self.allocator, verifier); + errdefer self.allocator.free(challenge); + + // 3. Build the URL. + const url = try std.fmt.allocPrint(self.allocator, + "https://accounts.google.com/o/oauth2/v2/auth?" ++ + "client_id={s}&" ++ + "redirect_uri={s}&" ++ + "response_type=code&" ++ + "scope=openid%20profile%20email&" ++ + "state={s}&" ++ + "code_challenge={s}&" ++ + "code_challenge_method=S256", + .{ self.client_id, self.redirect_uri, state, challenge } + ); + + return .{ + .url = url, + .code_verifier = verifier, + .code_challenge = challenge, + }; + } + + /// Exchanges the authorization code for an access token. + pub fn exchangeCode(self: *GoogleOAuth, code: []const u8, code_verifier: []const u8) !TokenResponse { + var client = std.http.Client{ + .allocator = self.allocator, + .io = self.io, + }; + defer client.deinit(); + + var response_body = std.ArrayList(u8).empty; + defer response_body.deinit(self.allocator); + + const body = try std.fmt.allocPrint(self.allocator, + "client_id={s}&" ++ + "client_secret={s}&" ++ + "code={s}&" ++ + "redirect_uri={s}&" ++ + "grant_type=authorization_code&" ++ + "code_verifier={s}", + .{ self.client_id, self.client_secret, code, self.redirect_uri, code_verifier } + ); + defer self.allocator.free(body); + + const result = try client.fetch(.{ + .location = .{ .url = "https://oauth2.googleapis.com/token" }, + .method = .POST, + .headers = .{ + .extra_headers = &.{ + .{ .name = "Content-Type", .value = "application/x-www-form-urlencoded" }, + }, + }, + .payload = body, + .response_writer = &response_body.writer(), + }); + + if (result.status.class() != .success) { + return error.GoogleOAuthFailed; + } + + // Parse the JSON response. + const parsed = try std.json.parseFromSlice(TokenResponse, self.allocator, response_body.items, .{}); + defer parsed.deinit(); + + return TokenResponse{ + .access_token = try self.allocator.dupe(u8, parsed.value.access_token), + .refresh_token = if (parsed.value.refresh_token) |rt| try self.allocator.dupe(u8, rt) else null, + .expires_in = parsed.value.expires_in, + .token_type = try self.allocator.dupe(u8, parsed.value.token_type), + .scope = try self.allocator.dupe(u8, parsed.value.scope), + .id_token = if (parsed.value.id_token) |id| try self.allocator.dupe(u8, id) else null, + }; + } + + pub fn freeTokenResponse(self: *GoogleOAuth, token: *TokenResponse) void { + self.allocator.free(@constCast(token.access_token)); + if (token.refresh_token) |rt| self.allocator.free(@constCast(rt)); + self.allocator.free(@constCast(token.token_type)); + self.allocator.free(@constCast(token.scope)); + if (token.id_token) |id| self.allocator.free(@constCast(id)); + } + + /// Fetches the authenticated user's info from Google. + pub fn fetchUser(self: *GoogleOAuth, access_token: []const u8) !User { + var client = std.http.Client{ + .allocator = self.allocator, + .io = self.io, + }; + defer client.deinit(); + + var response_body = std.ArrayList(u8).empty; + defer response_body.deinit(self.allocator); + + const auth_header = try std.fmt.allocPrint(self.allocator, "Bearer {s}", .{access_token}); + defer self.allocator.free(auth_header); + + const result = try client.fetch(.{ + .location = .{ .url = "https://www.googleapis.com/oauth2/v3/userinfo" }, + .method = .GET, + .headers = .{ + .extra_headers = &.{ + .{ .name = "Authorization", .value = auth_header }, + .{ .name = "Accept", .value = "application/json" }, + }, + }, + .response_writer = &response_body.writer(), + }); + + if (result.status.class() != .success) { + return error.GoogleUserFetchFailed; + } + + const parsed = try std.json.parseFromSlice(User, self.allocator, response_body.items, .{}); + defer parsed.deinit(); + + return User{ + .id = try self.allocator.dupe(u8, parsed.value.id), + .email = try self.allocator.dupe(u8, parsed.value.email), + .name = try self.allocator.dupe(u8, parsed.value.name), + .given_name = try self.allocator.dupe(u8, parsed.value.given_name), + .family_name = try self.allocator.dupe(u8, parsed.value.family_name), + .picture = if (parsed.value.picture) |p| try self.allocator.dupe(u8, p) else null, + }; + } + + pub fn freeUser(self: *GoogleOAuth, user: *User) void { + self.allocator.free(@constCast(user.id)); + self.allocator.free(@constCast(user.email)); + self.allocator.free(@constCast(user.name)); + self.allocator.free(@constCast(user.given_name)); + self.allocator.free(@constCast(user.family_name)); + if (user.picture) |p| self.allocator.free(@constCast(p)); + } +}; + +// Helper functions for PKCE. +fn generateCodeVerifier(allocator: std.mem.Allocator) ![]u8 { + const verifier_len = 64; + var verifier = try allocator.alloc(u8, verifier_len); + // Fill with random bytes (you'd need a cryptographically secure RNG). + std.crypto.random.bytes(verifier); + // Ensure it's URL-safe. + for (verifier) |*c| { + if (*c == '+' or *c == '/') *c = '-'; + } + return verifier; +} + +fn generateCodeChallenge(allocator: std.mem.Allocator, verifier: []const u8) ![]u8 { + // SHA256 hash the verifier. + var hash: [32]u8 = undefined; + std.crypto.hash.sha2.Sha256.hash(verifier, &hash, .{}); + + // Encode the hash as URL-safe base64. + const Encoder = std.base64.url_safe_no_pad.Base64Encoder; + var encoder = Encoder.init(); + const len = encoder.calcSize(hash.len); + const challenge = try allocator.alloc(u8, len); + _ = encoder.encode(challenge, &hash); + return challenge; +} From 87ffa9dd763a1925412739f6ffaebdaba41dd2d0 Mon Sep 17 00:00:00 2001 From: ZenDrx Date: Sun, 2 Aug 2026 07:11:44 +0100 Subject: [PATCH 7/8] Update comment for code verifier RNG requirement --- src/oauth/google.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/oauth/google.zig b/src/oauth/google.zig index 29da0f0..88a67c9 100644 --- a/src/oauth/google.zig +++ b/src/oauth/google.zig @@ -193,7 +193,7 @@ pub const GoogleOAuth = struct { fn generateCodeVerifier(allocator: std.mem.Allocator) ![]u8 { const verifier_len = 64; var verifier = try allocator.alloc(u8, verifier_len); - // Fill with random bytes (you'd need a cryptographically secure RNG). + // Fill with random bytes (we need a cryptographically secure RNG). std.crypto.random.bytes(verifier); // Ensure it's URL-safe. for (verifier) |*c| { From 421d62062b052af8919d78109969029cece02294 Mon Sep 17 00:00:00 2001 From: ZenDrx Date: Sun, 2 Aug 2026 07:21:56 +0100 Subject: [PATCH 8/8] Implement OIDCProvider for OpenID Connect support --- src/oauth/oidc.zig | 248 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 src/oauth/oidc.zig diff --git a/src/oauth/oidc.zig b/src/oauth/oidc.zig new file mode 100644 index 0000000..df16e0b --- /dev/null +++ b/src/oauth/oidc.zig @@ -0,0 +1,248 @@ +const std = @import("std"); + +pub const OIDCProvider = struct { + allocator: std.mem.Allocator, + io: std.Io, + client_id: []const u8, + client_secret: []const u8, + redirect_uri: []const u8, + issuer: []const u8, + + // Discovery document (fetched from .well-known/openid-configuration) + discovery: ?DiscoveryDocument = null, + + pub const DiscoveryDocument = struct { + issuer: []const u8, + authorization_endpoint: []const u8, + token_endpoint: []const u8, + userinfo_endpoint: []const u8, + jwks_uri: []const u8, + }; + + pub const User = struct { + sub: []const u8, + email: ?[]const u8, + email_verified: bool = false, + name: ?[]const u8, + given_name: ?[]const u8, + family_name: ?[]const u8, + picture: ?[]const u8, + locale: ?[]const u8, + }; + + pub const TokenResponse = struct { + access_token: []const u8, + refresh_token: ?[]const u8, + expires_in: u64, + token_type: []const u8, + scope: ?[]const u8, + id_token: ?[]const u8, + }; + + pub fn init( + allocator: std.mem.Allocator, + io: std.Io, + client_id: []const u8, + client_secret: []const u8, + redirect_uri: []const u8, + issuer: []const u8, + ) OIDCProvider { + return OIDCProvider{ + .allocator = allocator, + .io = io, + .client_id = client_id, + .client_secret = client_secret, + .redirect_uri = redirect_uri, + .issuer = issuer, + .discovery = null, + }; + } + + pub fn deinit(self: *OIDCProvider) void { + if (self.discovery) |*d| { + self.allocator.free(@constCast(d.issuer)); + self.allocator.free(@constCast(d.authorization_endpoint)); + self.allocator.free(@constCast(d.token_endpoint)); + self.allocator.free(@constCast(d.userinfo_endpoint)); + self.allocator.free(@constCast(d.jwks_uri)); + } + _ = self; + // The user manages their own strings + } + + /// Fetches the OpenID Connect discovery document from the issuer. + pub fn discover(self: *OIDCProvider) !void { + const well_known_url = try std.fmt.allocPrint(self.allocator, + "{s}/.well-known/openid-configuration", + .{self.issuer} + ); + defer self.allocator.free(well_known_url); + + var client = std.http.Client{ + .allocator = self.allocator, + .io = self.io, + }; + defer client.deinit(); + + var response_body = std.ArrayList(u8).empty; + defer response_body.deinit(self.allocator); + + const result = try client.fetch(.{ + .location = .{ .url = well_known_url }, + .method = .GET, + .headers = .{ + .extra_headers = &.{ + .{ .name = "Accept", .value = "application/json" }, + }, + }, + .response_writer = &response_body.writer(), + }); + + if (result.status.class() != .success) { + return error.OIDCDiscoveryFailed; + } + + // Parse the discovery document + const parsed = try std.json.parseFromSlice(DiscoveryDocument, self.allocator, response_body.items, .{}); + defer parsed.deinit(); + + self.discovery = DiscoveryDocument{ + .issuer = try self.allocator.dupe(u8, parsed.value.issuer), + .authorization_endpoint = try self.allocator.dupe(u8, parsed.value.authorization_endpoint), + .token_endpoint = try self.allocator.dupe(u8, parsed.value.token_endpoint), + .userinfo_endpoint = try self.allocator.dupe(u8, parsed.value.userinfo_endpoint), + .jwks_uri = try self.allocator.dupe(u8, parsed.value.jwks_uri), + }; + } + + /// Builds the authorization URL using the discovered endpoint. + pub fn authorizationUrl(self: *OIDCProvider, state: []const u8, scopes: []const u8) ![]const u8 { + if (self.discovery) |d| { + return try std.fmt.allocPrint(self.allocator, + "{s}?client_id={s}&redirect_uri={s}&response_type=code&scope={s}&state={s}", + .{ d.authorization_endpoint, self.client_id, self.redirect_uri, scopes, state } + ); + } else { + return error.OIDCNotDiscovered; + } + } + + /// Exchanges the authorization code for an access token. + pub fn exchangeCode(self: *OIDCProvider, code: []const u8) !TokenResponse { + if (self.discovery) |d| { + var client = std.http.Client{ + .allocator = self.allocator, + .io = self.io, + }; + defer client.deinit(); + + var response_body = std.ArrayList(u8).empty; + defer response_body.deinit(self.allocator); + + const body = try std.fmt.allocPrint(self.allocator, + "client_id={s}&client_secret={s}&code={s}&redirect_uri={s}&grant_type=authorization_code", + .{ self.client_id, self.client_secret, code, self.redirect_uri } + ); + defer self.allocator.free(body); + + const result = try client.fetch(.{ + .location = .{ .url = d.token_endpoint }, + .method = .POST, + .headers = .{ + .extra_headers = &.{ + .{ .name = "Content-Type", .value = "application/x-www-form-urlencoded" }, + .{ .name = "Accept", .value = "application/json" }, + }, + }, + .payload = body, + .response_writer = &response_body.writer(), + }); + + if (result.status.class() != .success) { + return error.OIDCTokenExchangeFailed; + } + + // Parse the token response + const parsed = try std.json.parseFromSlice(TokenResponse, self.allocator, response_body.items, .{}); + defer parsed.deinit(); + + return TokenResponse{ + .access_token = try self.allocator.dupe(u8, parsed.value.access_token), + .refresh_token = if (parsed.value.refresh_token) |rt| try self.allocator.dupe(u8, rt) else null, + .expires_in = parsed.value.expires_in, + .token_type = try self.allocator.dupe(u8, parsed.value.token_type), + .scope = if (parsed.value.scope) |s| try self.allocator.dupe(u8, s) else null, + .id_token = if (parsed.value.id_token) |id| try self.allocator.dupe(u8, id) else null, + }; + } else { + return error.OIDCNotDiscovered; + } + } + + pub fn freeTokenResponse(self: *OIDCProvider, token: *TokenResponse) void { + self.allocator.free(@constCast(token.access_token)); + if (token.refresh_token) |rt| self.allocator.free(@constCast(rt)); + self.allocator.free(@constCast(token.token_type)); + if (token.scope) |s| self.allocator.free(@constCast(s)); + if (token.id_token) |id| self.allocator.free(@constCast(id)); + } + + /// Fetches the authenticated user's info using the discovered endpoint. + pub fn fetchUser(self: *OIDCProvider, access_token: []const u8) !User { + if (self.discovery) |d| { + var client = std.http.Client{ + .allocator = self.allocator, + .io = self.io, + }; + defer client.deinit(); + + var response_body = std.ArrayList(u8).empty; + defer response_body.deinit(self.allocator); + + const auth_header = try std.fmt.allocPrint(self.allocator, "Bearer {s}", .{access_token}); + defer self.allocator.free(auth_header); + + const result = try client.fetch(.{ + .location = .{ .url = d.userinfo_endpoint }, + .method = .GET, + .headers = .{ + .extra_headers = &.{ + .{ .name = "Authorization", .value = auth_header }, + .{ .name = "Accept", .value = "application/json" }, + }, + }, + .response_writer = &response_body.writer(), + }); + + if (result.status.class() != .success) { + return error.OIDCUserFetchFailed; + } + + const parsed = try std.json.parseFromSlice(User, self.allocator, response_body.items, .{}); + defer parsed.deinit(); + + return User{ + .sub = try self.allocator.dupe(u8, parsed.value.sub), + .email = if (parsed.value.email) |e| try self.allocator.dupe(u8, e) else null, + .email_verified = parsed.value.email_verified, + .name = if (parsed.value.name) |n| try self.allocator.dupe(u8, n) else null, + .given_name = if (parsed.value.given_name) |g| try self.allocator.dupe(u8, g) else null, + .family_name = if (parsed.value.family_name) |f| try self.allocator.dupe(u8, f) else null, + .picture = if (parsed.value.picture) |p| try self.allocator.dupe(u8, p) else null, + .locale = if (parsed.value.locale) |l| try self.allocator.dupe(u8, l) else null, + }; + } else { + return error.OIDCNotDiscovered; + } + } + + pub fn freeUser(self: *OIDCProvider, user: *User) void { + self.allocator.free(@constCast(user.sub)); + if (user.email) |e| self.allocator.free(@constCast(e)); + if (user.name) |n| self.allocator.free(@constCast(n)); + if (user.given_name) |g| self.allocator.free(@constCast(g)); + if (user.family_name) |f| self.allocator.free(@constCast(f)); + if (user.picture) |p| self.allocator.free(@constCast(p)); + if (user.locale) |l| self.allocator.free(@constCast(l)); + } +};