-
Notifications
You must be signed in to change notification settings - Fork 1
/
oauth.$slug.ts
179 lines (165 loc) · 6 KB
/
oauth.$slug.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
// NOTE: This is purely a resource route, eventhough some part of it like the /authorize happens in the browser, it's not part of the
// host app. Any error for the browser flow (/authorize or /saml) can be set as a flash message and redirected to an error page for jackson
// Other errors(/userinfo and /token) can be thrown and should be caught by the host app CatchBoundary
import type { OAuthReq, OIDCAuthzResponsePayload } from "@boxyhq/saml-jackson";
import type { LoaderFunction, ActionFunction } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import invariant from "tiny-invariant";
import JacksonProvider, {
extractAuthTokenFromHeader,
} from "~/auth.jackson.server";
import {
commitSession,
getSession,
JACKSON_ERROR_COOKIE_KEY,
} from "~/sessions.server";
// Handles GET /api/oauth/authorize, GET /api/oauth/userinfo
export const loader: LoaderFunction = async ({ params, request }) => {
const session = await getSession(request.headers.get("Cookie"));
// maybe you change the name of the file to oauth.$somethingElse.ts, below invariant validates that the params is defined
invariant(params.slug, "expected params.slug");
const operation = params.slug;
if (
operation !== "authorize" &&
operation !== "oidc" &&
operation !== "userinfo"
) {
// Will be caught in CatchBoundary defined in root.tsx
throw new Response("Not Found", {
status: 404,
});
}
const url = new URL(request.url);
const { oauthController } = await JacksonProvider({
appBaseUrl: url.origin,
});
// rightmost query param will win in case of multiple ones with same name
const queryParams = Object.fromEntries(url.searchParams.entries());
switch (operation) {
case "authorize": {
try {
const { redirect_url, authorize_form } =
await oauthController.authorize(queryParams as unknown as OAuthReq);
if (redirect_url) {
// Redirect binding
return redirect(redirect_url, 302);
} else {
// POST Binding
return new Response(authorize_form, {
status: 200,
headers: {
"Content-Type": "text/html; charset=utf-8",
},
});
}
} catch (err: any) {
console.error("authorize error:", err);
const { message, statusCode = 500 } = err;
// set error in cookie redirect to error page
session.set(JACKSON_ERROR_COOKIE_KEY, { message, statusCode });
return redirect("/error", {
headers: { "Set-Cookie": await commitSession(session) },
});
}
}
case "oidc": {
try {
const { redirect_url } = await oauthController.oidcAuthzResponse(
queryParams as unknown as OIDCAuthzResponsePayload
);
if (redirect_url) {
return redirect(redirect_url, 302);
}
} catch (err: any) {
console.error("oidc callback error:", err);
const { message, statusCode = 500 } = err;
// set error in cookie redirect to error page
session.set(JACKSON_ERROR_COOKIE_KEY, { message, statusCode });
return redirect("/error", {
headers: { "Set-Cookie": await commitSession(session) },
});
}
}
case "userinfo": {
let token: string | null = extractAuthTokenFromHeader(request);
// check for query param
if (!token) {
token = queryParams.access_token;
}
if (!token) {
return new Response("Unauthorized", {
status: 401,
});
}
try {
const profile = await oauthController.userInfo(token);
return json(profile);
} catch (error: any) {
const { message, statusCode = 500 } = error;
throw new Response(message, { status: statusCode });
}
}
}
};
// Handles POST /api/oauth/saml, /api/oauth/oidc, POST /api/oauth/token
export const action: ActionFunction = async ({ params, request }) => {
const session = await getSession(request.headers.get("Cookie"));
// maybe you change the name of the file to $somethingElse.ts, below invariant validates that the params is defined
invariant(params.slug, "expected params.slug");
const operation = params.slug;
if (operation !== "saml" && operation !== "token") {
// Will be caught in CatchBoundary defined in root.tsx
throw new Response("Not Found", {
status: 404,
});
}
if (request.method !== "POST") {
// Will be caught in CatchBoundary defined in root.tsx
throw new Response("Method Not Allowed", { status: 405 });
}
let body;
const contentType = request.headers.get("Content-Type");
if (contentType === "application/x-www-form-urlencoded") {
body = Object.fromEntries(await request.formData());
} else if (contentType === "application/json") {
body = await request.json();
} else {
throw new Response("Unsupported Media Type", { status: 415 });
}
const url = new URL(request.url);
const { oauthController } = await JacksonProvider({
appBaseUrl: url.origin,
});
switch (operation) {
case "saml": {
try {
const { redirect_url, app_select_form } =
await oauthController.samlResponse(body);
if (redirect_url) {
return redirect(redirect_url, 302);
} else {
return new Response(app_select_form, {
headers: { "Content-Type": "text/html; charset=utf-8" },
});
}
} catch (err: any) {
console.error("saml callback error:", err);
const { message, statusCode = 500 } = err;
// set error in cookie redirect to error page
session.set(JACKSON_ERROR_COOKIE_KEY, { message, statusCode });
return redirect("/error", {
headers: { "Set-Cookie": await commitSession(session) },
});
}
}
case "token": {
try {
const tokenRes = await oauthController.token(body);
return json(tokenRes);
} catch (error: any) {
const { message, statusCode = 500 } = error;
throw new Response(message, { status: statusCode });
}
}
}
};