login route handler

This commit is contained in:
Max Peintner
2025-02-17 09:23:46 +01:00
parent 734426c116
commit ee898c447d

View File

@@ -100,6 +100,7 @@ export async function GET(request: NextRequest) {
sessions = await loadSessions({ serviceUrl, ids });
}
// complete flow if session and request id are provided
if (requestId && sessionId) {
if (requestId.startsWith("oidc_")) {
// this finishes the login process for OIDC
@@ -122,206 +123,251 @@ export async function GET(request: NextRequest) {
request,
});
}
}
if (requestId && requestId.startsWith("oidc_")) {
const { authRequest } = await getAuthRequest({
serviceUrl,
authRequestId: requestId.replace("oidc_", ""),
});
// continue with OIDC
if (requestId && requestId.startsWith("oidc_")) {
const { authRequest } = await getAuthRequest({
serviceUrl,
authRequestId: requestId.replace("oidc_", ""),
});
let organization = "";
let suffix = "";
let idpId = "";
let organization = "";
let suffix = "";
let idpId = "";
if (authRequest?.scope) {
const orgScope = authRequest.scope.find((s: string) =>
ORG_SCOPE_REGEX.test(s),
if (authRequest?.scope) {
const orgScope = authRequest.scope.find((s: string) =>
ORG_SCOPE_REGEX.test(s),
);
const idpScope = authRequest.scope.find((s: string) =>
IDP_SCOPE_REGEX.test(s),
);
if (orgScope) {
const matched = ORG_SCOPE_REGEX.exec(orgScope);
organization = matched?.[1] ?? "";
} else {
const orgDomainScope = authRequest.scope.find((s: string) =>
ORG_DOMAIN_SCOPE_REGEX.test(s),
);
const idpScope = authRequest.scope.find((s: string) =>
IDP_SCOPE_REGEX.test(s),
);
if (orgDomainScope) {
const matched = ORG_DOMAIN_SCOPE_REGEX.exec(orgDomainScope);
const orgDomain = matched?.[1] ?? "";
if (orgDomain) {
const orgs = await getOrgsByDomain({
serviceUrl,
if (orgScope) {
const matched = ORG_SCOPE_REGEX.exec(orgScope);
organization = matched?.[1] ?? "";
} else {
const orgDomainScope = authRequest.scope.find((s: string) =>
ORG_DOMAIN_SCOPE_REGEX.test(s),
);
if (orgDomainScope) {
const matched = ORG_DOMAIN_SCOPE_REGEX.exec(orgDomainScope);
const orgDomain = matched?.[1] ?? "";
if (orgDomain) {
const orgs = await getOrgsByDomain({
serviceUrl,
domain: orgDomain,
});
if (orgs.result && orgs.result.length === 1) {
organization = orgs.result[0].id ?? "";
suffix = orgDomain;
}
domain: orgDomain,
});
if (orgs.result && orgs.result.length === 1) {
organization = orgs.result[0].id ?? "";
suffix = orgDomain;
}
}
}
}
if (idpScope) {
const matched = IDP_SCOPE_REGEX.exec(idpScope);
idpId = matched?.[1] ?? "";
if (idpScope) {
const matched = IDP_SCOPE_REGEX.exec(idpScope);
idpId = matched?.[1] ?? "";
const identityProviders = await getActiveIdentityProviders({
const identityProviders = await getActiveIdentityProviders({
serviceUrl,
orgId: organization ? organization : undefined,
}).then((resp) => {
return resp.identityProviders;
});
const idp = identityProviders.find((idp) => idp.id === idpId);
if (idp) {
const origin = request.nextUrl.origin;
const identityProviderType = identityProviders[0].type;
let provider = idpTypeToSlug(identityProviderType);
const params = new URLSearchParams();
if (requestId) {
params.set("requestId", requestId);
}
if (organization) {
params.set("organization", organization);
}
return startIdentityProviderFlow({
serviceUrl,
orgId: organization ? organization : undefined,
idpId,
urls: {
successUrl:
`${origin}/idp/${provider}/success?` +
new URLSearchParams(params),
failureUrl:
`${origin}/idp/${provider}/failure?` +
new URLSearchParams(params),
},
}).then((resp) => {
return resp.identityProviders;
});
const idp = identityProviders.find((idp) => idp.id === idpId);
if (idp) {
const origin = request.nextUrl.origin;
const identityProviderType = identityProviders[0].type;
let provider = idpTypeToSlug(identityProviderType);
const params = new URLSearchParams();
if (requestId) {
params.set("requestId", requestId);
if (
resp.nextStep.value &&
typeof resp.nextStep.value === "string"
) {
return NextResponse.redirect(resp.nextStep.value);
}
});
}
}
}
if (authRequest && authRequest.prompt.includes(Prompt.CREATE)) {
const registerUrl = new URL("/register", request.url);
if (authRequest.id) {
registerUrl.searchParams.set("requestId", `oidc_${authRequest.id}`);
}
if (organization) {
registerUrl.searchParams.set("organization", organization);
}
return NextResponse.redirect(registerUrl);
}
// use existing session and hydrate it for oidc
if (authRequest && sessions.length) {
// if some accounts are available for selection and select_account is set
if (authRequest.prompt.includes(Prompt.SELECT_ACCOUNT)) {
return gotoAccounts({
request,
requestId: `oidc_${authRequest.id}`,
organization,
});
} else if (authRequest.prompt.includes(Prompt.LOGIN)) {
/**
* The login prompt instructs the authentication server to prompt the user for re-authentication, regardless of whether the user is already authenticated
*/
// if a hint is provided, skip loginname page and jump to the next page
if (authRequest.loginHint) {
try {
let command: SendLoginnameCommand = {
loginName: authRequest.loginHint,
requestId: authRequest.id,
};
if (organization) {
params.set("organization", organization);
command = { ...command, organization };
}
return startIdentityProviderFlow({
serviceUrl,
idpId,
urls: {
successUrl:
`${origin}/idp/${provider}/success?` +
new URLSearchParams(params),
failureUrl:
`${origin}/idp/${provider}/failure?` +
new URLSearchParams(params),
},
}).then((resp) => {
if (
resp.nextStep.value &&
typeof resp.nextStep.value === "string"
) {
return NextResponse.redirect(resp.nextStep.value);
}
});
const res = await sendLoginname(command);
if (res && "redirect" in res && res?.redirect) {
const absoluteUrl = new URL(res.redirect, request.url);
return NextResponse.redirect(absoluteUrl.toString());
}
} catch (error) {
console.error("Failed to execute sendLoginname:", error);
}
}
}
if (authRequest && authRequest.prompt.includes(Prompt.CREATE)) {
const registerUrl = new URL("/register", request.url);
const loginNameUrl = new URL("/loginname", request.url);
if (authRequest.id) {
registerUrl.searchParams.set("requestId", `oidc_${authRequest.id}`);
loginNameUrl.searchParams.set("requestId", `oidc_${authRequest.id}`);
}
if (authRequest.loginHint) {
loginNameUrl.searchParams.set("loginName", authRequest.loginHint);
}
if (organization) {
registerUrl.searchParams.set("organization", organization);
loginNameUrl.searchParams.set("organization", organization);
}
if (suffix) {
loginNameUrl.searchParams.set("suffix", suffix);
}
return NextResponse.redirect(loginNameUrl);
} else if (authRequest.prompt.includes(Prompt.NONE)) {
/**
* With an OIDC none prompt, the authentication server must not display any authentication or consent user interface pages.
* This means that the user should not be prompted to enter their password again.
* Instead, the server attempts to silently authenticate the user using an existing session or other authentication mechanisms that do not require user interaction
**/
const selectedSession = await findValidSession({
serviceUrl,
sessions,
authRequest,
});
if (!selectedSession || !selectedSession.id) {
return NextResponse.json(
{ error: "No active session found" },
{ status: 400 },
);
}
return NextResponse.redirect(registerUrl);
}
const cookie = sessionCookies.find(
(cookie) => cookie.id === selectedSession.id,
);
// use existing session and hydrate it for oidc
if (authRequest && sessions.length) {
// if some accounts are available for selection and select_account is set
if (authRequest.prompt.includes(Prompt.SELECT_ACCOUNT)) {
if (!cookie || !cookie.id || !cookie.token) {
return NextResponse.json(
{ error: "No active session found" },
{ status: 400 },
);
}
const session = {
sessionId: cookie.id,
sessionToken: cookie.token,
};
const { callbackUrl } = await createCallback({
serviceUrl,
req: create(CreateCallbackRequestSchema, {
authRequestId: requestId.replace("oidc_", ""),
callbackKind: {
case: "session",
value: create(SessionSchema, session),
},
}),
});
return NextResponse.redirect(callbackUrl);
} else {
// check for loginHint, userId hint and valid sessions
let selectedSession = await findValidSession({
serviceUrl,
sessions,
authRequest,
});
if (!selectedSession || !selectedSession.id) {
return gotoAccounts({
request,
requestId: `oidc_${authRequest.id}`,
organization,
});
} else if (authRequest.prompt.includes(Prompt.LOGIN)) {
/**
* The login prompt instructs the authentication server to prompt the user for re-authentication, regardless of whether the user is already authenticated
*/
}
// if a hint is provided, skip loginname page and jump to the next page
if (authRequest.loginHint) {
try {
let command: SendLoginnameCommand = {
loginName: authRequest.loginHint,
requestId: authRequest.id,
};
const cookie = sessionCookies.find(
(cookie) => cookie.id === selectedSession.id,
);
if (organization) {
command = { ...command, organization };
}
const res = await sendLoginname(command);
if (res && "redirect" in res && res?.redirect) {
const absoluteUrl = new URL(res.redirect, request.url);
return NextResponse.redirect(absoluteUrl.toString());
}
} catch (error) {
console.error("Failed to execute sendLoginname:", error);
}
}
const loginNameUrl = new URL("/loginname", request.url);
if (authRequest.id) {
loginNameUrl.searchParams.set(
"requestId",
`oidc_${authRequest.id}`,
);
}
if (authRequest.loginHint) {
loginNameUrl.searchParams.set("loginName", authRequest.loginHint);
}
if (organization) {
loginNameUrl.searchParams.set("organization", organization);
}
if (suffix) {
loginNameUrl.searchParams.set("suffix", suffix);
}
return NextResponse.redirect(loginNameUrl);
} else if (authRequest.prompt.includes(Prompt.NONE)) {
/**
* With an OIDC none prompt, the authentication server must not display any authentication or consent user interface pages.
* This means that the user should not be prompted to enter their password again.
* Instead, the server attempts to silently authenticate the user using an existing session or other authentication mechanisms that do not require user interaction
**/
const selectedSession = await findValidSession({
serviceUrl,
sessions,
authRequest,
if (!cookie || !cookie.id || !cookie.token) {
return gotoAccounts({
request,
requestId: `oidc_${authRequest.id}`,
organization,
});
}
if (!selectedSession || !selectedSession.id) {
return NextResponse.json(
{ error: "No active session found" },
{ status: 400 },
);
}
const cookie = sessionCookies.find(
(cookie) => cookie.id === selectedSession.id,
);
if (!cookie || !cookie.id || !cookie.token) {
return NextResponse.json(
{ error: "No active session found" },
{ status: 400 },
);
}
const session = {
sessionId: cookie.id,
sessionToken: cookie.token,
};
const session = {
sessionId: cookie.id,
sessionToken: cookie.token,
};
try {
const { callbackUrl } = await createCallback({
serviceUrl,
req: create(CreateCallbackRequestSchema, {
authRequestId: requestId.replace("oidc_", ""),
callbackKind: {
@@ -330,201 +376,155 @@ export async function GET(request: NextRequest) {
},
}),
});
return NextResponse.redirect(callbackUrl);
} else {
// check for loginHint, userId hint and valid sessions
let selectedSession = await findValidSession({
serviceUrl,
sessions,
authRequest,
});
if (!selectedSession || !selectedSession.id) {
return gotoAccounts({
request,
requestId: `oidc_${authRequest.id}`,
organization,
});
}
const cookie = sessionCookies.find(
(cookie) => cookie.id === selectedSession.id,
);
if (!cookie || !cookie.id || !cookie.token) {
return gotoAccounts({
request,
requestId: `oidc_${authRequest.id}`,
organization,
});
}
const session = {
sessionId: cookie.id,
sessionToken: cookie.token,
};
try {
const { callbackUrl } = await createCallback({
serviceUrl,
req: create(CreateCallbackRequestSchema, {
authRequestId: requestId.replace("oidc_", ""),
callbackKind: {
case: "session",
value: create(SessionSchema, session),
},
}),
});
if (callbackUrl) {
return NextResponse.redirect(callbackUrl);
} else {
console.log(
"could not create callback, redirect user to choose other account",
);
return gotoAccounts({
request,
organization,
requestId: `oidc_${authRequest.id}`,
});
}
} catch (error) {
console.error(error);
return gotoAccounts({
request,
requestId: `oidc_${authRequest.id}`,
organization,
});
}
}
} else {
const loginNameUrl = new URL("/loginname", request.url);
loginNameUrl.searchParams.set("requestId", requestId);
if (authRequest?.loginHint) {
loginNameUrl.searchParams.set("loginName", authRequest.loginHint);
loginNameUrl.searchParams.set("submit", "true"); // autosubmit
}
if (organization) {
loginNameUrl.searchParams.append("organization", organization);
// loginNameUrl.searchParams.set("organization", organization);
}
return NextResponse.redirect(loginNameUrl);
}
} else if (requestId && requestId.startsWith("saml_")) {
// handle saml request
const { samlRequest } = await getSAMLRequest({
serviceUrl,
samlRequestId: requestId.replace("saml_", ""),
});
if (!samlRequest) {
return NextResponse.json(
{ error: "No samlRequest found" },
{ status: 400 },
);
}
let selectedSession = await findValidSession({
serviceUrl,
sessions,
samlRequest,
});
if (!selectedSession || !selectedSession.id) {
return gotoAccounts({
request,
requestId: `saml_${samlRequest.id}`,
});
}
const cookie = sessionCookies.find(
(cookie) => cookie.id === selectedSession.id,
);
if (!cookie || !cookie.id || !cookie.token) {
return gotoAccounts({
request,
requestId: `saml_${samlRequest.id}`,
// organization,
});
}
const session = {
sessionId: cookie.id,
sessionToken: cookie.token,
};
try {
const { url, binding } = await createResponse({
serviceUrl,
req: create(CreateResponseRequestSchema, {
samlRequestId: requestId.replace("saml_", ""),
responseKind: {
case: "session",
value: session,
},
}),
});
if (url && binding.case === "redirect") {
return NextResponse.redirect(url);
} else if (url && binding.case === "post") {
const formData = {
key1: "value1",
key2: "value2",
};
// Convert form data to URL-encoded string
const formBody = Object.entries(formData)
.map(
([key, value]) =>
encodeURIComponent(key) + "=" + encodeURIComponent(value),
)
.join("&");
// Make a POST request to the external URL with the form data
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: formBody,
});
// Handle the response from the external URL
if (response.ok) {
return NextResponse.json({
message: "SAML request completed successfully",
});
if (callbackUrl) {
return NextResponse.redirect(callbackUrl);
} else {
return NextResponse.json(
{ error: "Failed to complete SAML request" },
{ status: response.status },
console.log(
"could not create callback, redirect user to choose other account",
);
return gotoAccounts({
request,
organization,
requestId: `oidc_${authRequest.id}`,
});
}
} else {
console.log(
"could not create response, redirect user to choose other account",
);
} catch (error) {
console.error(error);
return gotoAccounts({
request,
requestId: `saml_${samlRequest.id}`,
requestId: `oidc_${authRequest.id}`,
organization,
});
}
} catch (error) {
console.error(error);
}
} else {
const loginNameUrl = new URL("/loginname", request.url);
loginNameUrl.searchParams.set("requestId", requestId);
if (authRequest?.loginHint) {
loginNameUrl.searchParams.set("loginName", authRequest.loginHint);
loginNameUrl.searchParams.set("submit", "true"); // autosubmit
}
if (organization) {
loginNameUrl.searchParams.append("organization", organization);
// loginNameUrl.searchParams.set("organization", organization);
}
return NextResponse.redirect(loginNameUrl);
}
}
// continue with SAML
else if (requestId && requestId.startsWith("saml_")) {
const { samlRequest } = await getSAMLRequest({
serviceUrl,
samlRequestId: requestId.replace("saml_", ""),
});
if (!samlRequest) {
return NextResponse.json(
{ error: "No samlRequest found" },
{ status: 400 },
);
}
let selectedSession = await findValidSession({
serviceUrl,
sessions,
samlRequest,
});
if (!selectedSession || !selectedSession.id) {
return gotoAccounts({
request,
requestId: `saml_${samlRequest.id}`,
});
}
const cookie = sessionCookies.find(
(cookie) => cookie.id === selectedSession.id,
);
if (!cookie || !cookie.id || !cookie.token) {
return gotoAccounts({
request,
requestId: `saml_${samlRequest.id}`,
// organization,
});
}
const session = {
sessionId: cookie.id,
sessionToken: cookie.token,
};
try {
const { url, binding } = await createResponse({
serviceUrl,
req: create(CreateResponseRequestSchema, {
samlRequestId: requestId.replace("saml_", ""),
responseKind: {
case: "session",
value: session,
},
}),
});
if (url && binding.case === "redirect") {
return NextResponse.redirect(url);
} else if (url && binding.case === "post") {
const formData = {
key1: "value1",
key2: "value2",
};
// Convert form data to URL-encoded string
const formBody = Object.entries(formData)
.map(
([key, value]) =>
encodeURIComponent(key) + "=" + encodeURIComponent(value),
)
.join("&");
// Make a POST request to the external URL with the form data
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: formBody,
});
// Handle the response from the external URL
if (response.ok) {
return NextResponse.json({
message: "SAML request completed successfully",
});
} else {
return NextResponse.json(
{ error: "Failed to complete SAML request" },
{ status: response.status },
);
}
} else {
console.log(
"could not create response, redirect user to choose other account",
);
return gotoAccounts({
request,
requestId: `saml_${samlRequest.id}`,
});
}
} else {
return NextResponse.json(
{ error: "No authRequest nor samlRequest provided" },
{ status: 500 },
);
} catch (error) {
console.error(error);
return gotoAccounts({
request,
requestId: `saml_${samlRequest.id}`,
});
}
} else {
return NextResponse.json(
{ error: "No authRequest nor samlRequest provided" },
{ status: 500 },
);
}
}