authorgravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2023-08-26 11:41:14 -07:00
committergravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2023-08-26 11:41:14 -07:00
log4b0978e5becb22f8dee96a7c9861f2f5513bcd78
treeea3f431860e91d7a9118f314fb57355ea516c2f6
parentcf510ecdf568d4f88e75df8a71bdc584b11f980b

update to Zigmod r88


6 files changed, 358 insertions(+), 358 deletions(-)

build.zig+1-1
......@@ -12,7 +12,7 @@ pub fn build(b: *std.build.Builder) void {
1212 // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
1313 const mode = b.standardReleaseOptions();
1414
15 const exe = b.addExecutable("zig-oauth2", "src/main.zig");
15 const exe = b.addExecutable("zig-oauth2", "./main.zig");
1616 exe.setTarget(target);
1717 exe.setBuildMode(mode);
1818 deps.addAllTo(exe);
main.zig created+5
......@@ -0,0 +1,5 @@
1const std = @import("std");
2
3pub 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
3const std = @import("std");
4const string = []const u8;
5const files = @import("./files.zig");
6const pek = @import("pek");
7const zfetch = @import("zfetch");
8const extras = @import("extras");
9const UrlValues = @import("UrlValues");
10const Base = @This();
11
12pub 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
42pub const Client = struct {
43 provider: Provider,
44 id: string,
45 secret: string,
46};
47
48fn icon_url(comptime name: string) string {
49 return "https://unpkg.com/simple-icons@" ++ "5.13.0" ++ "/icons/" ++ name ++ ".svg";
50}
51
52pub 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
138pub 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
184pub 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
215pub 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
224pub const IsLoggedInFn = fn (*std.http.Server.Response) anyerror!bool;
225
226pub 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
307fn 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
325fn 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
330fn 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
339fn 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
348pub 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
3const std = @import("std");
4const string = []const u8;
5const files = @import("self/files");
6const pek = @import("pek");
7const zfetch = @import("zfetch");
8const extras = @import("extras");
9const UrlValues = @import("UrlValues");
10const Base = @This();
11
12pub 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
42pub const Client = struct {
43 provider: Provider,
44 id: string,
45 secret: string,
46};
47
48fn icon_url(comptime name: string) string {
49 return "https://unpkg.com/simple-icons@" ++ "5.13.0" ++ "/icons/" ++ name ++ ".svg";
50}
51
52pub 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
138pub 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
184pub 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
215pub 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
224pub const IsLoggedInFn = fn (*std.http.Server.Response) anyerror!bool;
225
226pub 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
307fn 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
325fn 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
330fn 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
339fn 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
348pub 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 @@
1const std = @import("std");
2
3pub fn main() anyerror!void {
4 std.log.info("All your codebase are belong to us.", .{});
5}
zig.mod+1-1
......@@ -1,6 +1,6 @@
11id: raz0lqnollhuhz5bq5n57pbo8cwm9l55z475i4ufcpzl57ez
22name: oauth2
3main: src/lib.zig
3main: oauth2.zig
44license: MIT
55description: HTTP handler functions to allow you to easily add OAuth2 login support to your Zig application
66files: