| ... | @@ -2,6 +2,12 @@ | ... | @@ -2,6 +2,12 @@ |
| 2 | // | 2 | // |
| 3 | const std = @import("std"); | 3 | const std = @import("std"); |
| 4 | const string = []const u8; | 4 | const string = []const u8; |
| | 5 | const http = @import("apple_pie"); |
| | 6 | const files = @import("self/files"); |
| | 7 | const pek = @import("pek"); |
| | 8 | const uri = @import("uri"); |
| | 9 | const zfetch = @import("zfetch"); |
| | 10 | const json = @import("json"); |
| 5 | | 11 | |
| 6 | pub const Provider = struct { | 12 | pub 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 | |
| | 177 | pub 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 | |
| | 186 | pub const IsLoggedInFn = fn (http.Request) anyerror!bool; |
| | 187 | |
| | 188 | pub 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 | |
| | 276 | fn 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 | |
| | 295 | fn 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 | |
| | 300 | fn 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 | |
| | 304 | fn 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 | |
| | 313 | fn 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 |
| | 323 | const 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 | }; |