| author | |
| committer | |
| log | 4b0978e5becb22f8dee96a7c9861f2f5513bcd78 |
| tree | ea3f431860e91d7a9118f314fb57355ea516c2f6 |
| parent | cf510ecdf568d4f88e75df8a71bdc584b11f980b |
6 files changed, 358 insertions(+), 358 deletions(-)
build.zig+1-1| ... | ... | @@ -12,7 +12,7 @@ pub fn build(b: *std.build.Builder) void { |
| 12 | 12 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. |
| 13 | 13 | const mode = b.standardReleaseOptions(); |
| 14 | 14 | |
| 15 | const exe = b.addExecutable("zig-oauth2", "src/main.zig"); | |
| 15 | const exe = b.addExecutable("zig-oauth2", "./main.zig"); | |
| 16 | 16 | exe.setTarget(target); |
| 17 | 17 | exe.setBuildMode(mode); |
| 18 | 18 | deps.addAllTo(exe); |
main.zig created+5| ... | ... | @@ -0,0 +1,5 @@ |
| 1 | const std = @import("std"); | |
| 2 | ||
| 3 | pub fn main() anyerror!void { | |
| 4 | std.log.info("All your codebase are belong to us.", .{}); | |
| 5 | } |
oauth2.zig created+351| ... | ... | @@ -0,0 +1,351 @@ |
| 1 | //! https://oauth.net/2/ | |
| 2 | ||
| 3 | const std = @import("std"); | |
| 4 | const string = []const u8; | |
| 5 | const files = @import("./files.zig"); | |
| 6 | const pek = @import("pek"); | |
| 7 | const zfetch = @import("zfetch"); | |
| 8 | const extras = @import("extras"); | |
| 9 | const UrlValues = @import("UrlValues"); | |
| 10 | const Base = @This(); | |
| 11 | ||
| 12 | pub const Provider = struct { | |
| 13 | id: string, | |
| 14 | authorize_url: string, | |
| 15 | token_url: string, | |
| 16 | me_url: string, | |
| 17 | scope: string = "", | |
| 18 | name_prop: string, | |
| 19 | name_prefix: string = "", | |
| 20 | id_prop: string = "id", | |
| 21 | logo: string, | |
| 22 | color: string, | |
| 23 | ||
| 24 | pub fn real_id(self: Provider) string { | |
| 25 | if (std.mem.indexOfScalar(u8, self.id, ',')) |_| { | |
| 26 | var iter = std.mem.splitScalar(u8, self.id, ','); | |
| 27 | return iter.next().?; | |
| 28 | } | |
| 29 | return self.id; | |
| 30 | } | |
| 31 | ||
| 32 | pub fn domain(self: Provider) string { | |
| 33 | if (std.mem.indexOfScalar(u8, self.id, ',')) |_| { | |
| 34 | var iter = std.mem.splitScalar(u8, self.id, ','); | |
| 35 | _ = iter.next(); | |
| 36 | return iter.next().?; | |
| 37 | } | |
| 38 | return self.id; | |
| 39 | } | |
| 40 | }; | |
| 41 | ||
| 42 | pub const Client = struct { | |
| 43 | provider: Provider, | |
| 44 | id: string, | |
| 45 | secret: string, | |
| 46 | }; | |
| 47 | ||
| 48 | fn icon_url(comptime name: string) string { | |
| 49 | return "https://unpkg.com/simple-icons@" ++ "5.13.0" ++ "/icons/" ++ name ++ ".svg"; | |
| 50 | } | |
| 51 | ||
| 52 | pub const providers = struct { | |
| 53 | pub var amazon = Provider{ | |
| 54 | .id = "amazon", | |
| 55 | .authorize_url = "https://www.amazon.com/ap/oa", | |
| 56 | .token_url = "https://api.amazon.com/auth/o2/token", | |
| 57 | .me_url = "https://api.amazon.com/user/profile", | |
| 58 | .scope = "profile", | |
| 59 | .name_prop = "name", | |
| 60 | .id_prop = "user_id", | |
| 61 | .logo = icon_url("amazon"), | |
| 62 | .color = "#FF9900", | |
| 63 | }; | |
| 64 | pub var battle_net = Provider{ | |
| 65 | .id = "battle.net", | |
| 66 | .authorize_url = "https://us.battle.net/oauth/authorize", | |
| 67 | .token_url = "https://us.battle.net/oauth/token", | |
| 68 | .me_url = "https://us.battle.net/oauth/userinfo", | |
| 69 | .scope = "openid", | |
| 70 | .name_prop = "battletag", | |
| 71 | .logo = icon_url("battle-dot-net"), | |
| 72 | .color = "#00AEFF", | |
| 73 | }; | |
| 74 | pub var discord = Provider{ | |
| 75 | .id = "discord", | |
| 76 | .authorize_url = "https://discordapp.com/api/oauth2/authorize", | |
| 77 | .token_url = "https://discordapp.com/api/oauth2/token", | |
| 78 | .me_url = "https://discordapp.com/api/users/@me", | |
| 79 | .scope = "identify", | |
| 80 | .name_prop = "username", | |
| 81 | .name_prefix = "@", | |
| 82 | .logo = icon_url("discord"), | |
| 83 | .color = "#7289DA", | |
| 84 | }; | |
| 85 | pub var facebook = Provider{ | |
| 86 | .id = "facebook", | |
| 87 | .authorize_url = "https://graph.facebook.com/oauth/authorize", | |
| 88 | .token_url = "https://graph.facebook.com/oauth/access_token", | |
| 89 | .me_url = "https://graph.facebook.com/me", | |
| 90 | .name_prop = "name", | |
| 91 | .logo = icon_url("facebook"), | |
| 92 | .color = "#1877F2", | |
| 93 | }; | |
| 94 | pub var github = Provider{ | |
| 95 | .id = "github.com", | |
| 96 | .authorize_url = "https://github.com/login/oauth/authorize", | |
| 97 | .token_url = "https://github.com/login/oauth/access_token", | |
| 98 | .me_url = "https://api.github.com/user", | |
| 99 | .scope = "read:user", | |
| 100 | .name_prop = "login", | |
| 101 | .name_prefix = "@", | |
| 102 | .logo = icon_url("github"), | |
| 103 | .color = "#181717", | |
| 104 | }; | |
| 105 | pub var google = Provider{ | |
| 106 | .id = "google", | |
| 107 | .authorize_url = "https://accounts.google.com/o/oauth2/v2/auth", | |
| 108 | .token_url = "https://www.googleapis.com/oauth2/v4/token", | |
| 109 | .me_url = "https://www.googleapis.com/oauth2/v1/userinfo?alt=json", | |
| 110 | .scope = "profile", | |
| 111 | .name_prop = "name", | |
| 112 | .logo = icon_url("google"), | |
| 113 | .color = "#4285F4", | |
| 114 | }; | |
| 115 | pub var microsoft = Provider{ | |
| 116 | .id = "microsoft", | |
| 117 | .authorize_url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", | |
| 118 | .token_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token", | |
| 119 | .me_url = "https://graph.microsoft.com/v1.0/me/", | |
| 120 | .scope = "https://graph.microsoft.com/user.read", | |
| 121 | .name_prop = "displayName", | |
| 122 | .logo = icon_url("microsoft"), | |
| 123 | .color = "#666666", | |
| 124 | }; | |
| 125 | pub var reddit = Provider{ | |
| 126 | .id = "reddit", | |
| 127 | .authorize_url = "https://old.reddit.com/api/v1/authorize", | |
| 128 | .token_url = "https://old.reddit.com/api/v1/access_token", | |
| 129 | .me_url = "https://oauth.reddit.com/api/v1/me", | |
| 130 | .scope = "identity", | |
| 131 | .name_prop = "name", | |
| 132 | .name_prefix = "u/", | |
| 133 | .logo = icon_url("reddit"), | |
| 134 | .color = "#FF4500", | |
| 135 | }; | |
| 136 | }; | |
| 137 | ||
| 138 | pub const dynamic_providers = struct { | |
| 139 | pub const _gitea = Provider{ | |
| 140 | .id = "gitea", | |
| 141 | .authorize_url = "https://{[domain]s}/login/oauth/authorize", | |
| 142 | .token_url = "https://{[domain]s}/login/oauth/access_token", | |
| 143 | .me_url = "https://{[domain]s}/api/v1/user", | |
| 144 | .name_prop = "username", | |
| 145 | .name_prefix = "@", | |
| 146 | .logo = icon_url("gitea"), | |
| 147 | .color = "#609926", | |
| 148 | }; | |
| 149 | pub const _gitlab = Provider{ | |
| 150 | .id = "gitlab", | |
| 151 | .authorize_url = "https://{[domain]s}/oauth/authorize", | |
| 152 | .token_url = "https://{[domain]s}/oauth/token", | |
| 153 | .me_url = "https://{[domain]s}/api/v4/user", | |
| 154 | .scope = "read_user", | |
| 155 | .name_prop = "username", | |
| 156 | .name_prefix = "@", | |
| 157 | .logo = icon_url("gitlab"), | |
| 158 | .color = "#FCA121", | |
| 159 | }; | |
| 160 | pub const _mastodon = Provider{ | |
| 161 | .id = "mastodon", | |
| 162 | .authorize_url = "https://{[domain]s}/oauth/authorize", | |
| 163 | .token_url = "https://{[domain]s}/oauth/token", | |
| 164 | .me_url = "https://{[domain]s}/api/v1/accounts/verify_credentials", | |
| 165 | .scope = "read:accounts", | |
| 166 | .name_prop = "username", | |
| 167 | .name_prefix = "@", | |
| 168 | .logo = icon_url("mastodon"), | |
| 169 | .color = "#3088D4", | |
| 170 | }; | |
| 171 | pub const _pleroma = Provider{ | |
| 172 | .id = "pleroma", | |
| 173 | .authorize_url = "https://{[domain]s}/oauth/authorize", | |
| 174 | .token_url = "https://{[domain]s}/oauth/token", | |
| 175 | .me_url = "https://{[domain]s}/api/v1/accounts/verify_credentials", | |
| 176 | .scope = "read:accounts", | |
| 177 | .name_prop = "username", | |
| 178 | .name_prefix = "@", | |
| 179 | .logo = icon_url("pleroma"), | |
| 180 | .color = "#FBA457", | |
| 181 | }; | |
| 182 | }; | |
| 183 | ||
| 184 | pub fn providerById(alloc: std.mem.Allocator, name: string) !?Provider { | |
| 185 | inline for (comptime std.meta.declarations(providers)) |item| { | |
| 186 | const p = @field(providers, item.name); | |
| 187 | if (std.mem.eql(u8, p.id, name)) { | |
| 188 | return p; | |
| 189 | } | |
| 190 | } | |
| 191 | const c_ind = std.mem.indexOfScalar(u8, name, ',') orelse return null; | |
| 192 | const p_id = name[0..c_ind]; | |
| 193 | const domain = name[c_ind + 1 ..]; | |
| 194 | const args = .{ .domain = domain }; | |
| 195 | inline for (comptime std.meta.declarations(dynamic_providers)) |item| { | |
| 196 | const didp = @field(dynamic_providers, item.name); | |
| 197 | if (std.mem.eql(u8, didp.id, p_id)) { | |
| 198 | return Provider{ | |
| 199 | .id = name, | |
| 200 | .authorize_url = try std.fmt.allocPrint(alloc, didp.authorize_url, args), | |
| 201 | .token_url = try std.fmt.allocPrint(alloc, didp.token_url, args), | |
| 202 | .me_url = try std.fmt.allocPrint(alloc, didp.me_url, args), | |
| 203 | .scope = didp.scope, | |
| 204 | .name_prop = didp.name_prop, | |
| 205 | .name_prefix = didp.name_prefix, | |
| 206 | .id_prop = didp.id_prop, | |
| 207 | .logo = didp.logo, | |
| 208 | .color = didp.color, | |
| 209 | }; | |
| 210 | } | |
| 211 | } | |
| 212 | return null; | |
| 213 | } | |
| 214 | ||
| 215 | pub fn clientByProviderId(clients: []const Client, name: string) ?Client { | |
| 216 | for (clients) |item| { | |
| 217 | if (std.mem.eql(u8, name, item.provider.id)) { | |
| 218 | return item; | |
| 219 | } | |
| 220 | } | |
| 221 | return null; | |
| 222 | } | |
| 223 | ||
| 224 | pub const IsLoggedInFn = fn (*std.http.Server.Response) anyerror!bool; | |
| 225 | ||
| 226 | pub fn Handlers(comptime T: type) type { | |
| 227 | comptime std.debug.assert(@hasDecl(T, "isLoggedIn")); | |
| 228 | comptime std.debug.assert(@hasDecl(T, "doneUrl")); | |
| 229 | comptime std.debug.assert(@hasDecl(T, "saveInfo")); | |
| 230 | comptime std.debug.assert(@hasDecl(T, "callbackPath")); | |
| 231 | ||
| 232 | return struct { | |
| 233 | const Self = @This(); | |
| 234 | pub var clients: []Client = &.{}; | |
| 235 | ||
| 236 | pub fn login(response: *std.http.Server.Response, body_writer: anytype, alloc: std.mem.Allocator, query: UrlValues) !void { | |
| 237 | if (query.get("with")) |with| { | |
| 238 | const client = clientByProviderId(Self.clients, with) orelse return try fail(response, body_writer, "Client with that ID not found!\n", .{}); | |
| 239 | return try loginOne(response, alloc, T, client, T.callbackPath); | |
| 240 | } | |
| 241 | if (Self.clients.len == 1) { | |
| 242 | return try loginOne(response, alloc, T, clients[0], T.callbackPath); | |
| 243 | } | |
| 244 | ||
| 245 | try response.headers.append("Content-Type", "text/html"); | |
| 246 | const page = files.@"/selector.pek"; | |
| 247 | const tmpl = comptime pek.parse(page); | |
| 248 | try pek.compile(Base, alloc, body_writer, tmpl, .{ | |
| 249 | .clients = Self.clients, | |
| 250 | }); | |
| 251 | } | |
| 252 | ||
| 253 | pub fn callback(response: *std.http.Server.Response, body_writer: anytype, alloc: std.mem.Allocator, query: UrlValues) !void { | |
| 254 | const state = query.get("state") orelse return try fail(response, body_writer, "", .{}); | |
| 255 | const client = clientByProviderId(Self.clients, state) orelse return try fail(response, body_writer, "error: No handler found for provider: {s}\n", .{state}); | |
| 256 | const code = query.get("code") orelse return try fail(response, body_writer, "", .{}); | |
| 257 | ||
| 258 | var params = UrlValues.init(alloc); | |
| 259 | try params.add("client_id", client.id); | |
| 260 | try params.add("client_secret", client.secret); | |
| 261 | try params.add("grant_type", "authorization_code"); | |
| 262 | try params.add("code", code); | |
| 263 | try params.add("redirect_uri", try redirectUri(response, alloc, T.callbackPath)); | |
| 264 | try params.add("state", "none"); | |
| 265 | ||
| 266 | const req = try zfetch.Request.init(alloc, client.provider.token_url, null); | |
| 267 | ||
| 268 | var headers = zfetch.Headers.init(alloc); | |
| 269 | try headers.appendValue("Content-Type", "application/x-www-form-urlencoded"); | |
| 270 | try headers.appendValue("Authorization", try std.fmt.allocPrint(alloc, "Basic {s}", .{try extras.base64EncodeAlloc(alloc, try std.mem.join(alloc, ":", &.{ client.id, client.secret }))})); | |
| 271 | try headers.appendValue("Accept", "application/json"); | |
| 272 | ||
| 273 | try req.do(.POST, headers, try params.encode()); | |
| 274 | const r = req.reader(); | |
| 275 | const body_content = try r.readAllAlloc(alloc, 1024 * 1024 * 5); | |
| 276 | if (req.status != .ok) std.log.scoped(.oauth).debug("{s}: {s}", .{ @tagName(req.status), body_content }); | |
| 277 | if (req.status != .ok) return error.OauthBadToken; | |
| 278 | const val = try extras.parse_json(alloc, body_content); | |
| 279 | ||
| 280 | const tt = val.value.object.get("token_type").?.string; | |
| 281 | if (!std.mem.eql(u8, tt, "bearer")) return fail(response, body_writer, "oauth2: invalid token type: {s}", .{tt}); | |
| 282 | ||
| 283 | const at = val.value.object.get("access_token") orelse return try fail(response, body_writer, "Identity Provider Login Error!\n{s}", .{body_content}); | |
| 284 | ||
| 285 | const req2 = try zfetch.Request.init(alloc, client.provider.me_url, null); | |
| 286 | var headers2 = zfetch.Headers.init(alloc); | |
| 287 | try headers2.appendValue("Authorization", try std.fmt.allocPrint(alloc, "Bearer {s}", .{at.string})); | |
| 288 | try headers2.appendValue("Accept", "application/json"); | |
| 289 | ||
| 290 | try req2.do(.GET, headers2, null); | |
| 291 | const r2 = req2.reader(); | |
| 292 | const body_content2 = try r2.readAllAlloc(alloc, 1024 * 1024 * 5); | |
| 293 | if (req2.status != .ok) std.log.scoped(.oauth).debug("{s}: {s}", .{ @tagName(req2.status), body_content2 }); | |
| 294 | if (req2.status != .ok) return error.OauthBadUserinfo; | |
| 295 | const val2 = try extras.parse_json(alloc, body_content2); | |
| 296 | ||
| 297 | const id = try fixId(alloc, val2.value.object.get(client.provider.id_prop).?); | |
| 298 | const name = val2.value.object.get(client.provider.name_prop).?.string; | |
| 299 | try T.saveInfo(response, alloc, client.provider, id, name, val.value, val2.value); | |
| 300 | ||
| 301 | try response.headers.append("Location", T.doneUrl); | |
| 302 | response.status = .found; | |
| 303 | } | |
| 304 | }; | |
| 305 | } | |
| 306 | ||
| 307 | fn loginOne(response: *std.http.Server.Response, alloc: std.mem.Allocator, comptime T: type, client: Client, callbackPath: string) !void { | |
| 308 | if (try T.isLoggedIn(response, alloc)) { | |
| 309 | try response.headers.append("Location", T.doneUrl); | |
| 310 | } else { | |
| 311 | const idp = client.provider; | |
| 312 | var params = UrlValues.init(alloc); | |
| 313 | try params.add("client_id", client.id); | |
| 314 | try params.add("redirect_uri", try redirectUri(response, alloc, callbackPath)); | |
| 315 | try params.add("response_type", "code"); | |
| 316 | try params.add("scope", idp.scope); | |
| 317 | try params.add("duration", "temporary"); | |
| 318 | try params.add("state", idp.id); | |
| 319 | const authurl = try std.mem.join(alloc, "?", &.{ idp.authorize_url, try params.encode() }); | |
| 320 | try response.headers.append("Location", authurl); | |
| 321 | } | |
| 322 | response.status = .found; | |
| 323 | } | |
| 324 | ||
| 325 | fn fail(response: *std.http.Server.Response, body_writer: anytype, comptime err: string, args: anytype) !void { | |
| 326 | response.status = .bad_request; | |
| 327 | try body_writer.print(err, args); | |
| 328 | } | |
| 329 | ||
| 330 | fn redirectUri(response: *std.http.Server.Response, alloc: std.mem.Allocator, callbackPath: string) !string { | |
| 331 | const headers = response.request.headers; | |
| 332 | const xproto = headers.getFirstValue("X-Forwarded-Proto") orelse ""; | |
| 333 | const maybe_tls = std.mem.eql(u8, xproto, "https"); | |
| 334 | const proto: string = if (maybe_tls) "https" else "http"; | |
| 335 | const host = response.request.headers.getFirstValue("host").?; | |
| 336 | return try std.fmt.allocPrint(alloc, "{s}://{s}{s}", .{ proto, host, callbackPath }); | |
| 337 | } | |
| 338 | ||
| 339 | fn fixId(alloc: std.mem.Allocator, id: std.json.Value) !string { | |
| 340 | return switch (id) { | |
| 341 | .string => |v| v, | |
| 342 | .integer => |v| try std.fmt.allocPrint(alloc, "{d}", .{v}), | |
| 343 | .float => |v| try std.fmt.allocPrint(alloc, "{d}", .{v}), | |
| 344 | else => unreachable, | |
| 345 | }; | |
| 346 | } | |
| 347 | ||
| 348 | pub fn pek_domain(alloc: std.mem.Allocator, writer: std.ArrayList(u8).Writer, p: Provider) !void { | |
| 349 | _ = alloc; | |
| 350 | try writer.writeAll(p.domain()); | |
| 351 | } |
src/lib.zig deleted-351| ... | ... | @@ -1,351 +0,0 @@ |
| 1 | //! https://oauth.net/2/ | |
| 2 | ||
| 3 | const std = @import("std"); | |
| 4 | const string = []const u8; | |
| 5 | const files = @import("self/files"); | |
| 6 | const pek = @import("pek"); | |
| 7 | const zfetch = @import("zfetch"); | |
| 8 | const extras = @import("extras"); | |
| 9 | const UrlValues = @import("UrlValues"); | |
| 10 | const Base = @This(); | |
| 11 | ||
| 12 | pub const Provider = struct { | |
| 13 | id: string, | |
| 14 | authorize_url: string, | |
| 15 | token_url: string, | |
| 16 | me_url: string, | |
| 17 | scope: string = "", | |
| 18 | name_prop: string, | |
| 19 | name_prefix: string = "", | |
| 20 | id_prop: string = "id", | |
| 21 | logo: string, | |
| 22 | color: string, | |
| 23 | ||
| 24 | pub fn real_id(self: Provider) string { | |
| 25 | if (std.mem.indexOfScalar(u8, self.id, ',')) |_| { | |
| 26 | var iter = std.mem.splitScalar(u8, self.id, ','); | |
| 27 | return iter.next().?; | |
| 28 | } | |
| 29 | return self.id; | |
| 30 | } | |
| 31 | ||
| 32 | pub fn domain(self: Provider) string { | |
| 33 | if (std.mem.indexOfScalar(u8, self.id, ',')) |_| { | |
| 34 | var iter = std.mem.splitScalar(u8, self.id, ','); | |
| 35 | _ = iter.next(); | |
| 36 | return iter.next().?; | |
| 37 | } | |
| 38 | return self.id; | |
| 39 | } | |
| 40 | }; | |
| 41 | ||
| 42 | pub const Client = struct { | |
| 43 | provider: Provider, | |
| 44 | id: string, | |
| 45 | secret: string, | |
| 46 | }; | |
| 47 | ||
| 48 | fn icon_url(comptime name: string) string { | |
| 49 | return "https://unpkg.com/simple-icons@" ++ "5.13.0" ++ "/icons/" ++ name ++ ".svg"; | |
| 50 | } | |
| 51 | ||
| 52 | pub const providers = struct { | |
| 53 | pub var amazon = Provider{ | |
| 54 | .id = "amazon", | |
| 55 | .authorize_url = "https://www.amazon.com/ap/oa", | |
| 56 | .token_url = "https://api.amazon.com/auth/o2/token", | |
| 57 | .me_url = "https://api.amazon.com/user/profile", | |
| 58 | .scope = "profile", | |
| 59 | .name_prop = "name", | |
| 60 | .id_prop = "user_id", | |
| 61 | .logo = icon_url("amazon"), | |
| 62 | .color = "#FF9900", | |
| 63 | }; | |
| 64 | pub var battle_net = Provider{ | |
| 65 | .id = "battle.net", | |
| 66 | .authorize_url = "https://us.battle.net/oauth/authorize", | |
| 67 | .token_url = "https://us.battle.net/oauth/token", | |
| 68 | .me_url = "https://us.battle.net/oauth/userinfo", | |
| 69 | .scope = "openid", | |
| 70 | .name_prop = "battletag", | |
| 71 | .logo = icon_url("battle-dot-net"), | |
| 72 | .color = "#00AEFF", | |
| 73 | }; | |
| 74 | pub var discord = Provider{ | |
| 75 | .id = "discord", | |
| 76 | .authorize_url = "https://discordapp.com/api/oauth2/authorize", | |
| 77 | .token_url = "https://discordapp.com/api/oauth2/token", | |
| 78 | .me_url = "https://discordapp.com/api/users/@me", | |
| 79 | .scope = "identify", | |
| 80 | .name_prop = "username", | |
| 81 | .name_prefix = "@", | |
| 82 | .logo = icon_url("discord"), | |
| 83 | .color = "#7289DA", | |
| 84 | }; | |
| 85 | pub var facebook = Provider{ | |
| 86 | .id = "facebook", | |
| 87 | .authorize_url = "https://graph.facebook.com/oauth/authorize", | |
| 88 | .token_url = "https://graph.facebook.com/oauth/access_token", | |
| 89 | .me_url = "https://graph.facebook.com/me", | |
| 90 | .name_prop = "name", | |
| 91 | .logo = icon_url("facebook"), | |
| 92 | .color = "#1877F2", | |
| 93 | }; | |
| 94 | pub var github = Provider{ | |
| 95 | .id = "github.com", | |
| 96 | .authorize_url = "https://github.com/login/oauth/authorize", | |
| 97 | .token_url = "https://github.com/login/oauth/access_token", | |
| 98 | .me_url = "https://api.github.com/user", | |
| 99 | .scope = "read:user", | |
| 100 | .name_prop = "login", | |
| 101 | .name_prefix = "@", | |
| 102 | .logo = icon_url("github"), | |
| 103 | .color = "#181717", | |
| 104 | }; | |
| 105 | pub var google = Provider{ | |
| 106 | .id = "google", | |
| 107 | .authorize_url = "https://accounts.google.com/o/oauth2/v2/auth", | |
| 108 | .token_url = "https://www.googleapis.com/oauth2/v4/token", | |
| 109 | .me_url = "https://www.googleapis.com/oauth2/v1/userinfo?alt=json", | |
| 110 | .scope = "profile", | |
| 111 | .name_prop = "name", | |
| 112 | .logo = icon_url("google"), | |
| 113 | .color = "#4285F4", | |
| 114 | }; | |
| 115 | pub var microsoft = Provider{ | |
| 116 | .id = "microsoft", | |
| 117 | .authorize_url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", | |
| 118 | .token_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token", | |
| 119 | .me_url = "https://graph.microsoft.com/v1.0/me/", | |
| 120 | .scope = "https://graph.microsoft.com/user.read", | |
| 121 | .name_prop = "displayName", | |
| 122 | .logo = icon_url("microsoft"), | |
| 123 | .color = "#666666", | |
| 124 | }; | |
| 125 | pub var reddit = Provider{ | |
| 126 | .id = "reddit", | |
| 127 | .authorize_url = "https://old.reddit.com/api/v1/authorize", | |
| 128 | .token_url = "https://old.reddit.com/api/v1/access_token", | |
| 129 | .me_url = "https://oauth.reddit.com/api/v1/me", | |
| 130 | .scope = "identity", | |
| 131 | .name_prop = "name", | |
| 132 | .name_prefix = "u/", | |
| 133 | .logo = icon_url("reddit"), | |
| 134 | .color = "#FF4500", | |
| 135 | }; | |
| 136 | }; | |
| 137 | ||
| 138 | pub const dynamic_providers = struct { | |
| 139 | pub const _gitea = Provider{ | |
| 140 | .id = "gitea", | |
| 141 | .authorize_url = "https://{[domain]s}/login/oauth/authorize", | |
| 142 | .token_url = "https://{[domain]s}/login/oauth/access_token", | |
| 143 | .me_url = "https://{[domain]s}/api/v1/user", | |
| 144 | .name_prop = "username", | |
| 145 | .name_prefix = "@", | |
| 146 | .logo = icon_url("gitea"), | |
| 147 | .color = "#609926", | |
| 148 | }; | |
| 149 | pub const _gitlab = Provider{ | |
| 150 | .id = "gitlab", | |
| 151 | .authorize_url = "https://{[domain]s}/oauth/authorize", | |
| 152 | .token_url = "https://{[domain]s}/oauth/token", | |
| 153 | .me_url = "https://{[domain]s}/api/v4/user", | |
| 154 | .scope = "read_user", | |
| 155 | .name_prop = "username", | |
| 156 | .name_prefix = "@", | |
| 157 | .logo = icon_url("gitlab"), | |
| 158 | .color = "#FCA121", | |
| 159 | }; | |
| 160 | pub const _mastodon = Provider{ | |
| 161 | .id = "mastodon", | |
| 162 | .authorize_url = "https://{[domain]s}/oauth/authorize", | |
| 163 | .token_url = "https://{[domain]s}/oauth/token", | |
| 164 | .me_url = "https://{[domain]s}/api/v1/accounts/verify_credentials", | |
| 165 | .scope = "read:accounts", | |
| 166 | .name_prop = "username", | |
| 167 | .name_prefix = "@", | |
| 168 | .logo = icon_url("mastodon"), | |
| 169 | .color = "#3088D4", | |
| 170 | }; | |
| 171 | pub const _pleroma = Provider{ | |
| 172 | .id = "pleroma", | |
| 173 | .authorize_url = "https://{[domain]s}/oauth/authorize", | |
| 174 | .token_url = "https://{[domain]s}/oauth/token", | |
| 175 | .me_url = "https://{[domain]s}/api/v1/accounts/verify_credentials", | |
| 176 | .scope = "read:accounts", | |
| 177 | .name_prop = "username", | |
| 178 | .name_prefix = "@", | |
| 179 | .logo = icon_url("pleroma"), | |
| 180 | .color = "#FBA457", | |
| 181 | }; | |
| 182 | }; | |
| 183 | ||
| 184 | pub fn providerById(alloc: std.mem.Allocator, name: string) !?Provider { | |
| 185 | inline for (comptime std.meta.declarations(providers)) |item| { | |
| 186 | const p = @field(providers, item.name); | |
| 187 | if (std.mem.eql(u8, p.id, name)) { | |
| 188 | return p; | |
| 189 | } | |
| 190 | } | |
| 191 | const c_ind = std.mem.indexOfScalar(u8, name, ',') orelse return null; | |
| 192 | const p_id = name[0..c_ind]; | |
| 193 | const domain = name[c_ind + 1 ..]; | |
| 194 | const args = .{ .domain = domain }; | |
| 195 | inline for (comptime std.meta.declarations(dynamic_providers)) |item| { | |
| 196 | const didp = @field(dynamic_providers, item.name); | |
| 197 | if (std.mem.eql(u8, didp.id, p_id)) { | |
| 198 | return Provider{ | |
| 199 | .id = name, | |
| 200 | .authorize_url = try std.fmt.allocPrint(alloc, didp.authorize_url, args), | |
| 201 | .token_url = try std.fmt.allocPrint(alloc, didp.token_url, args), | |
| 202 | .me_url = try std.fmt.allocPrint(alloc, didp.me_url, args), | |
| 203 | .scope = didp.scope, | |
| 204 | .name_prop = didp.name_prop, | |
| 205 | .name_prefix = didp.name_prefix, | |
| 206 | .id_prop = didp.id_prop, | |
| 207 | .logo = didp.logo, | |
| 208 | .color = didp.color, | |
| 209 | }; | |
| 210 | } | |
| 211 | } | |
| 212 | return null; | |
| 213 | } | |
| 214 | ||
| 215 | pub fn clientByProviderId(clients: []const Client, name: string) ?Client { | |
| 216 | for (clients) |item| { | |
| 217 | if (std.mem.eql(u8, name, item.provider.id)) { | |
| 218 | return item; | |
| 219 | } | |
| 220 | } | |
| 221 | return null; | |
| 222 | } | |
| 223 | ||
| 224 | pub const IsLoggedInFn = fn (*std.http.Server.Response) anyerror!bool; | |
| 225 | ||
| 226 | pub fn Handlers(comptime T: type) type { | |
| 227 | comptime std.debug.assert(@hasDecl(T, "isLoggedIn")); | |
| 228 | comptime std.debug.assert(@hasDecl(T, "doneUrl")); | |
| 229 | comptime std.debug.assert(@hasDecl(T, "saveInfo")); | |
| 230 | comptime std.debug.assert(@hasDecl(T, "callbackPath")); | |
| 231 | ||
| 232 | return struct { | |
| 233 | const Self = @This(); | |
| 234 | pub var clients: []Client = &.{}; | |
| 235 | ||
| 236 | pub fn login(response: *std.http.Server.Response, body_writer: anytype, alloc: std.mem.Allocator, query: UrlValues) !void { | |
| 237 | if (query.get("with")) |with| { | |
| 238 | const client = clientByProviderId(Self.clients, with) orelse return try fail(response, body_writer, "Client with that ID not found!\n", .{}); | |
| 239 | return try loginOne(response, alloc, T, client, T.callbackPath); | |
| 240 | } | |
| 241 | if (Self.clients.len == 1) { | |
| 242 | return try loginOne(response, alloc, T, clients[0], T.callbackPath); | |
| 243 | } | |
| 244 | ||
| 245 | try response.headers.append("Content-Type", "text/html"); | |
| 246 | const page = files.@"/selector.pek"; | |
| 247 | const tmpl = comptime pek.parse(page); | |
| 248 | try pek.compile(Base, alloc, body_writer, tmpl, .{ | |
| 249 | .clients = Self.clients, | |
| 250 | }); | |
| 251 | } | |
| 252 | ||
| 253 | pub fn callback(response: *std.http.Server.Response, body_writer: anytype, alloc: std.mem.Allocator, query: UrlValues) !void { | |
| 254 | const state = query.get("state") orelse return try fail(response, body_writer, "", .{}); | |
| 255 | const client = clientByProviderId(Self.clients, state) orelse return try fail(response, body_writer, "error: No handler found for provider: {s}\n", .{state}); | |
| 256 | const code = query.get("code") orelse return try fail(response, body_writer, "", .{}); | |
| 257 | ||
| 258 | var params = UrlValues.init(alloc); | |
| 259 | try params.add("client_id", client.id); | |
| 260 | try params.add("client_secret", client.secret); | |
| 261 | try params.add("grant_type", "authorization_code"); | |
| 262 | try params.add("code", code); | |
| 263 | try params.add("redirect_uri", try redirectUri(response, alloc, T.callbackPath)); | |
| 264 | try params.add("state", "none"); | |
| 265 | ||
| 266 | const req = try zfetch.Request.init(alloc, client.provider.token_url, null); | |
| 267 | ||
| 268 | var headers = zfetch.Headers.init(alloc); | |
| 269 | try headers.appendValue("Content-Type", "application/x-www-form-urlencoded"); | |
| 270 | try headers.appendValue("Authorization", try std.fmt.allocPrint(alloc, "Basic {s}", .{try extras.base64EncodeAlloc(alloc, try std.mem.join(alloc, ":", &.{ client.id, client.secret }))})); | |
| 271 | try headers.appendValue("Accept", "application/json"); | |
| 272 | ||
| 273 | try req.do(.POST, headers, try params.encode()); | |
| 274 | const r = req.reader(); | |
| 275 | const body_content = try r.readAllAlloc(alloc, 1024 * 1024 * 5); | |
| 276 | if (req.status != .ok) std.log.scoped(.oauth).debug("{s}: {s}", .{ @tagName(req.status), body_content }); | |
| 277 | if (req.status != .ok) return error.OauthBadToken; | |
| 278 | const val = try extras.parse_json(alloc, body_content); | |
| 279 | ||
| 280 | const tt = val.value.object.get("token_type").?.string; | |
| 281 | if (!std.mem.eql(u8, tt, "bearer")) return fail(response, body_writer, "oauth2: invalid token type: {s}", .{tt}); | |
| 282 | ||
| 283 | const at = val.value.object.get("access_token") orelse return try fail(response, body_writer, "Identity Provider Login Error!\n{s}", .{body_content}); | |
| 284 | ||
| 285 | const req2 = try zfetch.Request.init(alloc, client.provider.me_url, null); | |
| 286 | var headers2 = zfetch.Headers.init(alloc); | |
| 287 | try headers2.appendValue("Authorization", try std.fmt.allocPrint(alloc, "Bearer {s}", .{at.string})); | |
| 288 | try headers2.appendValue("Accept", "application/json"); | |
| 289 | ||
| 290 | try req2.do(.GET, headers2, null); | |
| 291 | const r2 = req2.reader(); | |
| 292 | const body_content2 = try r2.readAllAlloc(alloc, 1024 * 1024 * 5); | |
| 293 | if (req2.status != .ok) std.log.scoped(.oauth).debug("{s}: {s}", .{ @tagName(req2.status), body_content2 }); | |
| 294 | if (req2.status != .ok) return error.OauthBadUserinfo; | |
| 295 | const val2 = try extras.parse_json(alloc, body_content2); | |
| 296 | ||
| 297 | const id = try fixId(alloc, val2.value.object.get(client.provider.id_prop).?); | |
| 298 | const name = val2.value.object.get(client.provider.name_prop).?.string; | |
| 299 | try T.saveInfo(response, alloc, client.provider, id, name, val.value, val2.value); | |
| 300 | ||
| 301 | try response.headers.append("Location", T.doneUrl); | |
| 302 | response.status = .found; | |
| 303 | } | |
| 304 | }; | |
| 305 | } | |
| 306 | ||
| 307 | fn loginOne(response: *std.http.Server.Response, alloc: std.mem.Allocator, comptime T: type, client: Client, callbackPath: string) !void { | |
| 308 | if (try T.isLoggedIn(response, alloc)) { | |
| 309 | try response.headers.append("Location", T.doneUrl); | |
| 310 | } else { | |
| 311 | const idp = client.provider; | |
| 312 | var params = UrlValues.init(alloc); | |
| 313 | try params.add("client_id", client.id); | |
| 314 | try params.add("redirect_uri", try redirectUri(response, alloc, callbackPath)); | |
| 315 | try params.add("response_type", "code"); | |
| 316 | try params.add("scope", idp.scope); | |
| 317 | try params.add("duration", "temporary"); | |
| 318 | try params.add("state", idp.id); | |
| 319 | const authurl = try std.mem.join(alloc, "?", &.{ idp.authorize_url, try params.encode() }); | |
| 320 | try response.headers.append("Location", authurl); | |
| 321 | } | |
| 322 | response.status = .found; | |
| 323 | } | |
| 324 | ||
| 325 | fn fail(response: *std.http.Server.Response, body_writer: anytype, comptime err: string, args: anytype) !void { | |
| 326 | response.status = .bad_request; | |
| 327 | try body_writer.print(err, args); | |
| 328 | } | |
| 329 | ||
| 330 | fn redirectUri(response: *std.http.Server.Response, alloc: std.mem.Allocator, callbackPath: string) !string { | |
| 331 | const headers = response.request.headers; | |
| 332 | const xproto = headers.getFirstValue("X-Forwarded-Proto") orelse ""; | |
| 333 | const maybe_tls = std.mem.eql(u8, xproto, "https"); | |
| 334 | const proto: string = if (maybe_tls) "https" else "http"; | |
| 335 | const host = response.request.headers.getFirstValue("host").?; | |
| 336 | return try std.fmt.allocPrint(alloc, "{s}://{s}{s}", .{ proto, host, callbackPath }); | |
| 337 | } | |
| 338 | ||
| 339 | fn fixId(alloc: std.mem.Allocator, id: std.json.Value) !string { | |
| 340 | return switch (id) { | |
| 341 | .string => |v| v, | |
| 342 | .integer => |v| try std.fmt.allocPrint(alloc, "{d}", .{v}), | |
| 343 | .float => |v| try std.fmt.allocPrint(alloc, "{d}", .{v}), | |
| 344 | else => unreachable, | |
| 345 | }; | |
| 346 | } | |
| 347 | ||
| 348 | pub fn pek_domain(alloc: std.mem.Allocator, writer: std.ArrayList(u8).Writer, p: Provider) !void { | |
| 349 | _ = alloc; | |
| 350 | try writer.writeAll(p.domain()); | |
| 351 | } |
src/main.zig deleted-5| ... | ... | @@ -1,5 +0,0 @@ |
| 1 | const std = @import("std"); | |
| 2 | ||
| 3 | pub fn main() anyerror!void { | |
| 4 | std.log.info("All your codebase are belong to us.", .{}); | |
| 5 | } |
zig.mod+1-1| ... | ... | @@ -1,6 +1,6 @@ |
| 1 | 1 | id: raz0lqnollhuhz5bq5n57pbo8cwm9l55z475i4ufcpzl57ez |
| 2 | 2 | name: oauth2 |
| 3 | main: src/lib.zig | |
| 3 | main: oauth2.zig | |
| 4 | 4 | license: MIT |
| 5 | 5 | description: HTTP handler functions to allow you to easily add OAuth2 login support to your Zig application |
| 6 | 6 | files: |