authorgravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2026-06-12 14:46:46 -07:00
committergravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2026-06-12 14:46:46 -07:00
logf92478c7972e8daa48878a68fc1d43dc2646daa0
tree6dcf40906b5ba7d7dd0d49c8e0f257a64652af23
parentb9eee6396695f32b59a9eada10524e488c0d60c1
signaturebadge-check Signed by SSH key SHA256:4hHJbtBRU58AYXwjL7fkz2fnQHdiye8x1QpTCQ0sHNw

add OpenID Connect provider support


2 files changed, 55 insertions(+), 1 deletions(-)

README.md+1
......@@ -24,4 +24,5 @@ HTTP handler functions to allow you to easily add OAuth2 login support to your Z
2424- GitLab
2525- Mastodon
2626- Pleroma
27- OpenID Connect
2728- ... and custom ones!
oauth2.zig+54-1
......@@ -239,6 +239,59 @@ pub fn providerById(alloc: std.mem.Allocator, name: string) !?Provider {
239239 };
240240 }
241241 }
242 if (std.mem.eql(u8, p_id, "oidc")) {
243 const io = root.io;
244 var buf: [4096]u8 = @splat(0);
245 var http_client: std.http.Client = .{ .allocator = alloc, .io = io };
246 defer http_client.deinit();
247
248 const url_s = try nio.fmt.allocPrint(alloc, "https://{s}/.well-known/openid-configuration", .{domain});
249 defer alloc.free(url_s);
250
251 var req = try http_client.request(.GET, try std.Uri.parse(url_s), .{
252 .headers = .{
253 .accept_encoding = .{ .override = "identity" },
254 },
255 .redirect_behavior = .not_allowed,
256 });
257 defer req.deinit();
258 try req.sendBodiless();
259 var resp = try req.receiveHead(&.{});
260 const body_content = try resp.reader(&buf).allocRemaining(alloc, .limited(1024 * 1024 * 5));
261 defer alloc.free(body_content);
262 if (resp.head.status != .ok) std.log.scoped(.oauth).err("GET '{s}': {d}", .{ url_s, resp.head.status });
263 if (resp.head.status != .ok) std.log.scoped(.oauth).err("{s}", .{body_content});
264 if (resp.head.status != .ok) return null;
265 const val = try json.parseFromSlice(alloc, "body.json", body_content, .{ .maximum_depth = 100, .support_trailing_commas = true });
266 defer val.deinit(alloc);
267 val.acquire();
268 defer val.release();
269 const rootobj = val.root.object();
270 const authorize_url = rootobj.getS("authorization_endpoint") orelse {
271 std.log.scoped(.oauth).err("openid-configuration did not have the expected 'authorization_endpoint' key", .{});
272 return null;
273 };
274 const token_url = rootobj.getS("token_endpoint") orelse {
275 std.log.scoped(.oauth).err("openid-configuration did not have the expected 'token_endpoint' key", .{});
276 return null;
277 };
278 const me_url = rootobj.getS("userinfo_endpoint") orelse {
279 std.log.scoped(.oauth).err("openid-configuration did not have the expected 'userinfo_endpoint' key", .{});
280 return null;
281 };
282 return Provider{
283 .id = name,
284 .authorize_url = try alloc.dupe(u8, authorize_url),
285 .token_url = try alloc.dupe(u8, token_url),
286 .me_url = try alloc.dupe(u8, me_url),
287 .scope = "openid",
288 .name_prop = "name",
289 .name_prefix = "",
290 .id_prop = "sub",
291 .logo = icon_url("openid"),
292 .color = "#F78C40",
293 };
294 }
242295 inline for (comptime extras.globalOption("oauth2_dynamic_providers", []const Provider) orelse &.{}) |didp| {
243296 if (std.mem.eql(u8, didp.id, p_id)) {
244297 return Provider{
......@@ -329,7 +382,7 @@ pub fn Handlers(comptime T: type) type {
329382 const val = try json.parseFromSlice(alloc, "body.json", body_content, .{ .maximum_depth = 100, .support_trailing_commas = true });
330383 val.acquire();
331384 const tt = val.root.object().getS("token_type").?;
332 if (!std.mem.eql(u8, tt, "bearer")) return fail(response_status, body_writer, "oauth2: invalid token type: {s}", .{tt});
385 if (!std.ascii.eqlIgnoreCase(tt, "bearer")) return fail(response_status, body_writer, "oauth2: invalid token type: expected 'bearer', got '{s}'", .{tt});
333386 const at = val.root.object().getS("access_token") orelse return try fail(response_status, body_writer, "Identity Provider Login Error!\n{s}", .{body_content});
334387 val.release();
335388