| 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,7 +12,7 @@ pub fn build(b: *std.build.Builder) void { |
| 12 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. | 12 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. |
| 13 | const mode = b.standardReleaseOptions(); | 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 | exe.setTarget(target); | 16 | exe.setTarget(target); |
| 17 | exe.setBuildMode(mode); | 17 | exe.setBuildMode(mode); |
| 18 | deps.addAllTo(exe); | 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,6 +1,6 @@ |
| 1 | id: raz0lqnollhuhz5bq5n57pbo8cwm9l55z475i4ufcpzl57ez | 1 | id: raz0lqnollhuhz5bq5n57pbo8cwm9l55z475i4ufcpzl57ez |
| 2 | name: oauth2 | 2 | name: oauth2 |
| 3 | main: src/lib.zig | 3 | main: oauth2.zig |
| 4 | license: MIT | 4 | license: MIT |
| 5 | description: HTTP handler functions to allow you to easily add OAuth2 login support to your Zig application | 5 | description: HTTP handler functions to allow you to easily add OAuth2 login support to your Zig application |
| 6 | files: | 6 | files: |