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:
commit
49a8e702bc
@ -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",
|
||||||
|
@ -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":
|
||||||
|
@ -94,4 +94,5 @@ export type ClassKey =
|
|||||||
| "kcSelectOTPListItemClass"
|
| "kcSelectOTPListItemClass"
|
||||||
| "kcAuthenticatorOtpCircleClass"
|
| "kcAuthenticatorOtpCircleClass"
|
||||||
| "kcSelectOTPItemHeadingClass"
|
| "kcSelectOTPItemHeadingClass"
|
||||||
| "kcFormOptionsWrapperClass";
|
| "kcFormOptionsWrapperClass"
|
||||||
|
| "kcFormButtonsWrapperClass";
|
||||||
|
@ -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: {
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
68
src/login/pages/LoginDeviceVerifyUserCode.tsx
Normal file
68
src/login/pages/LoginDeviceVerifyUserCode.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
73
src/login/pages/LoginOauthGrant.tsx
Normal file
73
src/login/pages/LoginOauthGrant.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
24
stories/login/pages/LoginDeviceVerifyUserCode.stories.tsx
Normal file
24
stories/login/pages/LoginDeviceVerifyUserCode.stories.tsx
Normal 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 />;
|
24
stories/login/pages/LoginOauthGrant.stories.tsx
Normal file
24
stories/login/pages/LoginOauthGrant.stories.tsx
Normal 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 />;
|
Loading…
x
Reference in New Issue
Block a user