Merge pull request #447 from celinepelletier/add-oauth2-device-flow-pages

feat: add login-oauth2-device-verify-user-code and login-oauth-grant pages
This commit is contained in:
Joseph Garrone 2023-11-04 00:14:52 +01:00 committed by GitHub
commit 49a8e702bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 245 additions and 2 deletions

View File

@ -10,6 +10,8 @@ export const loginThemePageIds = [
"login-reset-password.ftl", "login-reset-password.ftl",
"login-verify-email.ftl", "login-verify-email.ftl",
"terms.ftl", "terms.ftl",
"login-oauth2-device-verify-user-code.ftl",
"login-oauth-grant.ftl",
"login-otp.ftl", "login-otp.ftl",
"login-update-profile.ftl", "login-update-profile.ftl",
"login-update-password.ftl", "login-update-password.ftl",

View File

@ -12,6 +12,8 @@ const Error = lazy(() => import("keycloakify/login/pages/Error"));
const LoginResetPassword = lazy(() => import("keycloakify/login/pages/LoginResetPassword")); const LoginResetPassword = lazy(() => import("keycloakify/login/pages/LoginResetPassword"));
const LoginVerifyEmail = lazy(() => import("keycloakify/login/pages/LoginVerifyEmail")); const LoginVerifyEmail = lazy(() => import("keycloakify/login/pages/LoginVerifyEmail"));
const Terms = lazy(() => import("keycloakify/login/pages/Terms")); const Terms = lazy(() => import("keycloakify/login/pages/Terms"));
const LoginDeviceVerifyUserCode = lazy(() => import("keycloakify/login/pages/LoginDeviceVerifyUserCode"));
const LoginOauthGrant = lazy(() => import("keycloakify/login/pages/LoginOauthGrant"));
const LoginOtp = lazy(() => import("keycloakify/login/pages/LoginOtp")); const LoginOtp = lazy(() => import("keycloakify/login/pages/LoginOtp"));
const LoginPassword = lazy(() => import("keycloakify/login/pages/LoginPassword")); const LoginPassword = lazy(() => import("keycloakify/login/pages/LoginPassword"));
const LoginUsername = lazy(() => import("keycloakify/login/pages/LoginUsername")); const LoginUsername = lazy(() => import("keycloakify/login/pages/LoginUsername"));
@ -52,6 +54,10 @@ export default function Fallback(props: PageProps<KcContext, I18n>) {
return <LoginVerifyEmail kcContext={kcContext} {...rest} />; return <LoginVerifyEmail kcContext={kcContext} {...rest} />;
case "terms.ftl": case "terms.ftl":
return <Terms kcContext={kcContext} {...rest} />; return <Terms kcContext={kcContext} {...rest} />;
case "login-oauth2-device-verify-user-code.ftl":
return <LoginDeviceVerifyUserCode kcContext={kcContext} {...rest} />;
case "login-oauth-grant.ftl":
return <LoginOauthGrant kcContext={kcContext} {...rest} />;
case "login-otp.ftl": case "login-otp.ftl":
return <LoginOtp kcContext={kcContext} {...rest} />; return <LoginOtp kcContext={kcContext} {...rest} />;
case "login-username.ftl": case "login-username.ftl":

View File

@ -94,4 +94,5 @@ export type ClassKey =
| "kcSelectOTPListItemClass" | "kcSelectOTPListItemClass"
| "kcAuthenticatorOtpCircleClass" | "kcAuthenticatorOtpCircleClass"
| "kcSelectOTPItemHeadingClass" | "kcSelectOTPItemHeadingClass"
| "kcFormOptionsWrapperClass"; | "kcFormOptionsWrapperClass"
| "kcFormButtonsWrapperClass";

View File

@ -18,6 +18,8 @@ export type KcContext =
| KcContext.LoginResetPassword | KcContext.LoginResetPassword
| KcContext.LoginVerifyEmail | KcContext.LoginVerifyEmail
| KcContext.Terms | KcContext.Terms
| KcContext.LoginDeviceVerifyUserCode
| KcContext.LoginOauthGrant
| KcContext.LoginOtp | KcContext.LoginOtp
| KcContext.LoginUsername | KcContext.LoginUsername
| KcContext.WebauthnAuthenticate | KcContext.WebauthnAuthenticate
@ -241,6 +243,27 @@ export declare namespace KcContext {
pageId: "terms.ftl"; pageId: "terms.ftl";
}; };
export type LoginDeviceVerifyUserCode = Common & {
pageId: "login-oauth2-device-verify-user-code.ftl";
url: {
oauth2DeviceVerificationAction: string;
};
};
export type LoginOauthGrant = Common & {
pageId: "login-oauth-grant.ftl";
oauth: {
code: string;
client: string;
clientScopesRequested: {
consentScreenText: string;
}[];
};
url: {
oauthAction: string;
};
};
export type LoginOtp = Common & { export type LoginOtp = Common & {
pageId: "login-otp.ftl"; pageId: "login-otp.ftl";
otpLogin: { otpLogin: {

View File

@ -240,7 +240,9 @@ export const kcContextCommonMock: KcContext.Common = {
const loginUrl = { const loginUrl = {
...kcContextCommonMock.url, ...kcContextCommonMock.url,
"loginResetCredentialsUrl": "/auth/realms/myrealm/login-actions/reset-credentials?client_id=account&tab_id=HoAx28ja4xg", "loginResetCredentialsUrl": "/auth/realms/myrealm/login-actions/reset-credentials?client_id=account&tab_id=HoAx28ja4xg",
"registrationUrl": "/auth/realms/myrealm/login-actions/registration?client_id=account&tab_id=HoAx28ja4xg" "registrationUrl": "/auth/realms/myrealm/login-actions/registration?client_id=account&tab_id=HoAx28ja4xg",
"oauth2DeviceVerificationAction": "/auth/realms/myrealm/device",
"oauthAction": "/auth/realms/myrealm/login-actions/consent?client_id=account&tab_id=HoAx28ja4xg"
}; };
export const kcContextMocks = [ export const kcContextMocks = [
@ -344,6 +346,25 @@ export const kcContextMocks = [
...kcContextCommonMock, ...kcContextCommonMock,
"pageId": "terms.ftl" "pageId": "terms.ftl"
}), }),
id<KcContext.LoginDeviceVerifyUserCode>({
...kcContextCommonMock,
"pageId": "login-oauth2-device-verify-user-code.ftl",
url: loginUrl
}),
id<KcContext.LoginOauthGrant>({
...kcContextCommonMock,
"pageId": "login-oauth-grant.ftl",
oauth: {
code: "5-1N4CIzfi1aprIQjmylI-9e3spLCWW9i5d-GDcs-Sw",
clientScopesRequested: [
{ consentScreenText: "${profileScopeConsentText}" },
{ consentScreenText: "${rolesScopeConsentText}" },
{ consentScreenText: "${emailScopeConsentText}" }
],
client: "account"
},
url: loginUrl
}),
id<KcContext.LoginOtp>({ id<KcContext.LoginOtp>({
...kcContextCommonMock, ...kcContextCommonMock,
"pageId": "login-otp.ftl", "pageId": "login-otp.ftl",

View File

@ -45,6 +45,7 @@ export const { useGetClassName } = createUseClassName<ClassKey>({
"kcInputClass": "form-control", "kcInputClass": "form-control",
"kcInputErrorMessageClass": "pf-c-form__helper-text pf-m-error required kc-feedback-text", "kcInputErrorMessageClass": "pf-c-form__helper-text pf-m-error required kc-feedback-text",
"kcInputWrapperClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12", "kcInputWrapperClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
"kcFormButtonsWrapperClass": undefined,
"kcFormOptionsClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12", "kcFormOptionsClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
"kcFormButtonsClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12", "kcFormButtonsClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
"kcFormSettingClass": "login-pf-settings", "kcFormSettingClass": "login-pf-settings",

View File

@ -0,0 +1,68 @@
import { clsx } from "keycloakify/tools/clsx";
import Template from "../Template";
import { I18n } from "../i18n";
import { KcContext } from "../kcContext";
import { useGetClassName } from "../lib/useGetClassName";
import { PageProps } from "./PageProps";
export default function LoginOauthGrant(props: PageProps<Extract<KcContext, { pageId: "login-oauth2-device-verify-user-code.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, classes } = props;
const { url } = kcContext;
const { msg, msgStr } = i18n;
const { getClassName } = useGetClassName({
doUseDefaultCss,
classes
});
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("oauth2DeviceVerificationTitle")}>
<form
id="kc-user-verify-device-user-code-form"
className={getClassName("kcFormClass")}
action={url.oauth2DeviceVerificationAction}
method="post"
>
<div className={getClassName("kcFormGroupClass")}>
<div className={getClassName("kcLabelWrapperClass")}>
<label htmlFor="device-user-code" className={getClassName("kcLabelClass")}>
{msg("verifyOAuth2DeviceUserCode")}
</label>
</div>
<div className={getClassName("kcInputWrapperClass")}>
<input
id="device-user-code"
name="device_user_code"
autoComplete="off"
type="text"
className={getClassName("kcInputClass")}
autoFocus
/>
</div>
</div>
<div className={getClassName("kcFormGroupClass")}>
<div id="kc-form-options" className={getClassName("kcFormOptionsClass")}>
<div className={getClassName("kcFormOptionsWrapperClass")}></div>
</div>
<div id="kc-form-buttons" className={getClassName("kcFormButtonsClass")}>
<div className={getClassName("kcFormButtonsWrapperClass")}>
<input
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonPrimaryClass"),
getClassName("kcButtonLargeClass")
)}
type="submit"
value={msgStr("doSubmit")}
/>
</div>
</div>
</div>
</form>
</Template>
);
}

View File

@ -0,0 +1,73 @@
import { clsx } from "keycloakify/tools/clsx";
import { PageProps } from "./PageProps";
import { KcContext } from "../kcContext";
import { I18n } from "../i18n";
import Template from "../Template";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
export default function LoginOauthGrant(props: PageProps<Extract<KcContext, { pageId: "login-oauth-grant.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, classes } = props;
const { url, oauth, client } = kcContext;
const { msg, msgStr, advancedMsg, advancedMsgStr } = i18n;
const { getClassName } = useGetClassName({
doUseDefaultCss,
classes
});
return (
<Template
{...{ kcContext, i18n, doUseDefaultCss, classes }}
headerNode={msg("oauthGrantTitle", client.name ? advancedMsgStr(client.name) : client.clientId)}
>
<div id="kc-oauth" className="content-area">
<h3>{msg("oauthGrantRequest")}</h3>
<ul>
{oauth.clientScopesRequested.map(clientScope => (
<li key={clientScope.consentScreenText}>
<span>{advancedMsg(clientScope.consentScreenText)}</span>
</li>
))}
</ul>
<form className="form-actions" action={url.oauthAction} method="POST">
<input type="hidden" name="code" value={oauth.code} />
<div className={getClassName("kcFormGroupClass")}>
<div id="kc-form-options">
<div className={getClassName("kcFormOptionsWrapperClass")}></div>
</div>
<div id="kc-form-buttons">
<div className={getClassName("kcFormButtonsWrapperClass")}>
<input
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonPrimaryClass"),
getClassName("kcButtonLargeClass")
)}
name="accept"
id="kc-login"
type="submit"
value={msgStr("doYes")}
/>
<input
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonDefaultClass"),
getClassName("kcButtonLargeClass")
)}
name="cancel"
id="kc-cancel"
type="submit"
value={msgStr("doNo")}
/>
</div>
</div>
</div>
</form>
<div className="clearfix"></div>
</div>
</Template>
);
}

View File

@ -0,0 +1,24 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "login-oauth2-device-verify-user-code.ftl";
const { PageStory } = createPageStory({ pageId });
const meta: ComponentMeta<any> = {
title: `login/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
"hidden": true
}
}
}
};
export default meta;
export const Default = () => <PageStory />;

View File

@ -0,0 +1,24 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "login-oauth-grant.ftl";
const { PageStory } = createPageStory({ pageId });
const meta: ComponentMeta<any> = {
title: `login/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
"hidden": true
}
}
}
};
export default meta;
export const Default = () => <PageStory />;