authorgravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2021-09-11 23:57:58 -07:00
committergravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2021-09-11 23:57:58 -07:00
logb506dd942bd1a43a89fb46ab9f3ca6386fc381e8
treea110fb7af4ee8747b229845fa3d8f369f525be96
parent23a6c92d0729d8bae6b6e774c6333304094d1fc0

add handler implementation


3 files changed, 229 insertions(+), 0 deletions(-)

src/lib.zig+178
...@@ -2,6 +2,12 @@...@@ -2,6 +2,12 @@
2//2//
3const std = @import("std");3const std = @import("std");
4const string = []const u8;4const string = []const u8;
5const http = @import("apple_pie");
6const files = @import("self/files");
7const pek = @import("pek");
8const uri = @import("uri");
9const zfetch = @import("zfetch");
10const json = @import("json");
511
6pub const Provider = struct {12pub const Provider = struct {
7 id: string,13 id: string,
...@@ -167,3 +173,175 @@ pub fn providerById(name: string) ?Provider {...@@ -167,3 +173,175 @@ pub fn providerById(name: string) ?Provider {
167 }173 }
168 return null;174 return null;
169}175}
176
177pub fn clientByProviderId(clients: []const Client, name: string) ?Client {
178 for (clients) |item| {
179 if (std.mem.eql(u8, name, item.provider.id)) {
180 return item;
181 }
182 }
183 return null;
184}
185
186pub const IsLoggedInFn = fn (http.Request) anyerror!bool;
187
188pub fn Handlers(comptime T: type) type {
189 comptime std.debug.assert(@hasDecl(T, "Ctx"));
190 comptime std.debug.assert(@hasDecl(T, "isLoggedIn"));
191 comptime std.debug.assert(@hasDecl(T, "doneUrl"));
192 comptime std.debug.assert(@hasDecl(T, "saveInfo"));
193
194 return struct {
195 const Self = @This();
196 pub var clients: []Client = &.{};
197 pub var callbackPath: string = "";
198
199 pub fn login(_: T.Ctx, response: *http.Response, request: http.Request, args: struct {}) !void {
200 _ = args;
201
202 const alloc = request.arena;
203 const query = try request.context.url.queryParameters(alloc);
204
205 if (query.get("with")) |with| {
206 const client = clientByProviderId(Self.clients, with) orelse return try fail(response, "Client with that ID not found!\n", .{});
207 return try loginOne(response, request, T, client, Self.callbackPath);
208 }
209
210 if (Self.clients.len == 1) {
211 return try loginOne(response, request, T, clients[0], Self.callbackPath);
212 }
213
214 try response.headers.put("Content-Type", "text/html");
215 const page = comptime files.open("/selector.pek").?;
216 const tmpl = comptime pek.parse(page);
217 try pek.compile(alloc, response.writer(), tmpl, .{
218 .clients = Self.clients,
219 });
220 }
221
222 pub fn callback(_: T.Ctx, response: *http.Response, request: http.Request, args: struct {}) !void {
223 _ = args;
224
225 const alloc = request.arena;
226 const query = try request.context.url.queryParameters(alloc);
227
228 const state = query.get("state") orelse return try fail(response, "", .{});
229 const client = clientByProviderId(Self.clients, state) orelse return try fail(response, "error: No handler found for provider: {s}\n", .{state});
230 const code = query.get("code") orelse return try fail(response, "", .{});
231
232 var params = UrlValues.init(alloc);
233 try params.add("client_id", client.id);
234 try params.add("client_secret", client.secret);
235 try params.add("grant_type", "authorization_code");
236 try params.add("code", code);
237 try params.add("redirect_uri", try redirectUri(alloc, request, callbackPath));
238 try params.add("state", "none");
239
240 const req = try zfetch.Request.init(alloc, client.provider.token_url, null);
241
242 var headers = zfetch.Headers.init(alloc);
243 try headers.appendValue("Content-Type", "application/x-www-form-urlencoded");
244 try headers.appendValue("Authorization", try std.fmt.allocPrint(alloc, "Basic {s}", .{try base64EncodeAlloc(alloc, try std.mem.join(alloc, ":", &.{ client.id, client.secret }))}));
245 try headers.appendValue("Accept", "application/json");
246
247 // TODO print error message to response if this fails
248 try req.do(.POST, headers, try params.encode());
249 const r = req.reader();
250 const body_content = try r.readAllAlloc(alloc, 1024 * 1024 * 5);
251 const val = try json.parse(alloc, body_content);
252
253 const at = val.get("access_token") orelse return try fail(response, "Identity Provider Login Error!\n{s}", .{body_content});
254
255 const req2 = try zfetch.Request.init(alloc, client.provider.me_url, null);
256 var headers2 = zfetch.Headers.init(alloc);
257 try headers2.appendValue("Authorization", try std.fmt.allocPrint(alloc, "Bearer {s}", .{at.String}));
258 try headers2.appendValue("Accept", "application/json");
259
260 // TODO print error message if this fails
261 try req2.do(.GET, headers2, null);
262 const r2 = req2.reader();
263 const body_content2 = try r2.readAllAlloc(alloc, 1024 * 1024 * 5);
264 const val2 = try json.parse(alloc, body_content2);
265
266 const id = try fixId(alloc, val2.get(client.provider.id_prop).?);
267 const name = val2.get(client.provider.name_prop).?.String;
268 try T.saveInfo(response, request, client.provider, id, name, val2);
269
270 try response.headers.put("Location", T.doneUrl);
271 try response.writeHeader(.found);
272 }
273 };
274}
275
276fn loginOne(response: *http.Response, request: http.Request, comptime T: type, client: Client, callbackPath: string) !void {
277 if (try T.isLoggedIn(request)) {
278 try response.headers.put("Location", T.doneUrl);
279 } else {
280 const alloc = request.arena;
281 const idp = client.provider;
282 var params = UrlValues.init(alloc);
283 try params.add("client_id", client.id);
284 try params.add("redirect_uri", try redirectUri(alloc, request, callbackPath));
285 try params.add("response_type", "code");
286 try params.add("scope", idp.scope);
287 try params.add("duration", "temporary");
288 try params.add("state", idp.id);
289 const authurl = try std.mem.join(alloc, "?", &.{ idp.authorize_url, try params.encode() });
290 try response.headers.put("Location", authurl);
291 }
292 try response.writeHeader(.found);
293}
294
295fn fail(response: *http.Response, comptime err: string, args: anytype) !void {
296 try response.writeHeader(.bad_request);
297 try response.writer().print(err, args);
298}
299
300fn redirectUri(alloc: *std.mem.Allocator, request: http.Request, callbackPath: string) !string {
301 return try std.fmt.allocPrint(alloc, "http://{s}{s}", .{ request.host().?, callbackPath });
302}
303
304fn fixId(alloc: *std.mem.Allocator, id: json.Value) !string {
305 return switch (id) {
306 .String => |v| return v,
307 .Int => |v| return try std.fmt.allocPrint(alloc, "{d}", .{v}),
308 .Float => |v| return try std.fmt.allocPrint(alloc, "{d}", .{v}),
309 else => unreachable,
310 };
311}
312
313fn base64EncodeAlloc(alloc: *std.mem.Allocator, input: string) !string {
314 const base64 = std.base64.standard_encoder;
315 var buf = try alloc.alloc(u8, base64.calcSize(input.len));
316 return base64.encode(buf, input);
317}
318
319//
320//
321
322// TODO make this its own library
323const UrlValues = struct {
324 inner: std.StringArrayHashMap(string),
325
326 pub fn init(alloc: *std.mem.Allocator) UrlValues {
327 return .{
328 .inner = std.StringArrayHashMap(string).init(alloc),
329 };
330 }
331
332 pub fn add(self: *UrlValues, key: string, value: string) !void {
333 try self.inner.putNoClobber(key, value);
334 }
335
336 pub fn encode(self: UrlValues) !string {
337 const alloc = self.inner.allocator;
338 var list = std.ArrayList(u8).init(alloc);
339 var iter = self.inner.iterator();
340 var i: usize = 0;
341 while (iter.next()) |entry| : (i += 1) {
342 if (i > 0) try list.writer().writeAll("&");
343 try list.writer().print("{s}={s}", .{ entry.key_ptr.*, uri.escapeString(alloc, entry.value_ptr.*) });
344 }
345 return list.toOwnedSlice();
346 }
347};
www/selector.pek created+43
...@@ -0,0 +1,43 @@
1html[lang="en"](
2 head(
3 title("Select An Identity Provider")
4 meta[charset="UTF-8"]
5 meta[http-equiv="X-UA-Compatible" content="IE=edge"]
6 meta[name="viewport" content="width=device-width,initial-scale=1"]
7 link[rel="icon" href="data:,"]
8 link[rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.7.5/semantic.min.css" integrity="sha256-S4n5rcKkPwT9YZGXPue8OorJ7GCPxBA5o/Z0ALWXyHs=" crossorigin="anonymous"]
9 style("
10 body > div { margin: 1em; }
11 body { max-width: 75em; }
12 form .ui.button { margin: 1em 0; }
13 .prov { display: inline-flex; flex-direction: column-reverse; justify-content: center; align-items: center; margin: 1em; }
14 label { display: flex; flex-direction: column; }
15 object { display: block; }
16 ")
17 )
18 body(
19 div[class="ui main menu"](
20 div[class="right item"](
21 span("Powered by")
22 a[href="https://github.com/nektro/zig-oauth2"]("ZigOAuth2")
23 )
24 )
25 div(
26 form[action="login" method="get"](
27 fieldset(
28 legend("Select a Provider:")
29 {#each clients}
30 div[class="prov"](
31 input[type="radio" name="with" value=({this.provider.id}) id=("p-"{this.provider.id})]
32 label[for=("p-"{this.provider.id})](
33 object[type="image/svg+xml" height="48" data=({this.provider.logo})]
34 div({this.provider.id})
35 )
36 )
37 /each/
38 )
39 button[class="ui button" type="submit"]("Login")
40 )
41 )
42 )
43)
zig.mod+8
...@@ -3,4 +3,12 @@ name: oauth2...@@ -3,4 +3,12 @@ name: oauth2
3main: src/lib.zig3main: src/lib.zig
4license: MIT4license: MIT
5description: HTTP handler functions to allow you to easily add OAuth2 login support to your Zig application5description: HTTP handler functions to allow you to easily add OAuth2 login support to your Zig application
6files:
7 - www
6dependencies:8dependencies:
9 - src: git https://github.com/Luukdegram/apple_pie
10 - src: git https://github.com/nektro/zig-pek
11 - src: git https://github.com/MasterQ32/zig-uri
12 - src: git https://github.com/nektro/iguanaTLS # temporary
13 - src: git https://github.com/truemedian/zfetch
14 - src: git https://github.com/nektro/zig-json