device code flow

This commit is contained in:
Max Peintner
2025-05-02 15:08:41 +02:00
parent 5274c2bd7d
commit 6270cf9522
5 changed files with 70 additions and 14 deletions

View File

@@ -190,7 +190,7 @@
"device": {
"usercode": {
"title": "Device code",
"description": "Enter the code provided in the verification email.",
"description": "Enter the code.",
"submit": "Continue"
},
"request": {

View File

@@ -5,9 +5,11 @@ import { UserAvatar } from "@/components/user-avatar";
import { getMostRecentCookieWithLoginname } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service";
import {
authorizeOrDenyDeviceAuthorization,
createCallback,
createResponse,
getBrandingSettings,
getDeviceAuthorizationRequest,
getLoginSettings,
getSession,
} from "@/lib/zitadel";
@@ -24,7 +26,6 @@ import { redirect } from "next/navigation";
async function loadSession(
serviceUrl: string,
loginName: string,
requestId?: string,
) {
@@ -62,6 +63,26 @@ async function loadSession(
}).then(({ url }) => {
return redirect(url);
});
} else if (requestId && requestId.startsWith("device_")) {
const userCode = requestId.replace("device_", "");
const { deviceAuthorizationRequest } = await getDeviceAuthorizationRequest({
serviceUrl,
userCode,
});
if (!deviceAuthorizationRequest) {
throw new Error("Device authorization request not found");
}
return authorizeOrDenyDeviceAuthorization({
serviceUrl,
deviceAuthorizationId: deviceAuthorizationRequest?.id,
session: {
sessionId: recent.id,
sessionToken: recent.token,
},
});
}
return getSession({

View File

@@ -127,6 +127,7 @@ export async function GET(request: NextRequest) {
request,
});
} else if (requestId.startsWith("device_")) {
// this finishes the login process for Device Authorization
return loginWithDeviceAndSession({
serviceUrl,
deviceRequest: requestId.replace("device_", ""),
@@ -509,7 +510,9 @@ export async function GET(request: NextRequest) {
requestId: `saml_${samlRequest.id}`,
});
}
} else {
}
// Device Authorization does not need to start here as it is handled on the /device endpoint
else {
return NextResponse.json(
{ error: "No authRequest nor samlRequest provided" },
{ status: 500 },

View File

@@ -51,7 +51,7 @@ export function DeviceCodeForm({ userCode }: { userCode?: string }) {
return router.push(
`/device/consent?` +
new URLSearchParams({
requestId: `device_${response.deviceAuthorizationRequest.id}`,
requestId: `device_${userCode}`,
user_code: value.userCode,
}).toString(),
);

View File

@@ -5,6 +5,28 @@ type FinishFlowCommand =
}
| { loginName: string };
function goToSignedInPage(
props:
| { sessionId: string; organization?: string }
| { organization?: string; loginName: string },
) {
const params = new URLSearchParams({});
if ("loginName" in props && props.loginName) {
params.append("loginName", props.loginName);
}
if ("sessionId" in props && props.sessionId) {
params.append("sessionId", props.sessionId);
}
if (props.organization) {
params.append("organization", props.organization);
}
return `/signedin?` + params;
}
/**
* for client: redirects user back to an OIDC or SAML application or to a success page when using requestId, check if a default redirect and redirect to it, or just redirect to a success page with the loginName
* @param command
@@ -14,7 +36,25 @@ export async function getNextUrl(
command: FinishFlowCommand & { organization?: string },
defaultRedirectUri?: string,
): Promise<string> {
if ("sessionId" in command && "requestId" in command) {
// finish Device Authorization Flow
if (
"requestId" in command &&
command.requestId.startsWith("device_") &&
("loginName" in command || "sessionId" in command)
) {
return goToSignedInPage({
...command,
organization: command.organization,
});
}
// finish SAML or OIDC flow
if (
"sessionId" in command &&
"requestId" in command &&
(command.requestId.startsWith("saml_") ||
command.requestId.startsWith("oidc_"))
) {
const params = new URLSearchParams({
sessionId: command.sessionId,
requestId: command.requestId,
@@ -31,13 +71,5 @@ export async function getNextUrl(
return defaultRedirectUri;
}
const params = new URLSearchParams({
loginName: command.loginName,
});
if (command.organization) {
params.append("organization", command.organization);
}
return `/signedin?` + params;
return goToSignedInPage(command);
}