diff --git a/README.md b/README.md index 5c73e9d..72bfc6c 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ keycloak-modern-login is a Keycloak login theme that's build using Tailwind CSS, ![](docs/login-form.jpg) +![](docs/select-authenticator.jpg) + ![](docs/login-webauthn.jpg) diff --git a/docker-compose.yaml b/docker-compose.yaml index a2c89c5..8397665 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -12,4 +12,5 @@ services: - KEYCLOAK_ADMIN_PASSWORD=admin command: - start-dev + - --features=recovery-codes - --import-realm diff --git a/docs/select-authenticator.jpg b/docs/select-authenticator.jpg new file mode 100644 index 0000000..1b6e022 Binary files /dev/null and b/docs/select-authenticator.jpg differ diff --git a/package.json b/package.json index cf7ae9b..490b5de 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "prod": "webpack --mode=production", "zipjar": "cd dist; zip -r ../dist/theme.jar META-INF theme; cd -", "build": "yarn prod && yarn zipjar", - "format": "prettier --write .", + "format": "prettier --write src/", "lint": "eslint --cache --report-unused-disable-directives --format codeframe --ext js,ts,vue ." }, "devDependencies": { diff --git a/src/functions/utils.ts b/src/functions/utils.ts new file mode 100644 index 0000000..4a28423 --- /dev/null +++ b/src/functions/utils.ts @@ -0,0 +1,22 @@ +export function formPost(url: string, data: object) { + const form = document.createElement("form"); + form.method = "post"; + form.action = url; + + for (const key in data) { + if (Object.prototype.hasOwnProperty.call(data, key)) { + if (data[key] === undefined) { + continue; + } + + const hiddenField = document.createElement("input"); + hiddenField.type = "hidden"; + hiddenField.name = key; + hiddenField.value = data[key]; + + form.appendChild(hiddenField); + } + } + document.body.appendChild(form); + form.submit(); +} diff --git a/src/types/context.ts b/src/types/context.ts index 1a7c3a8..348afbb 100644 --- a/src/types/context.ts +++ b/src/types/context.ts @@ -31,6 +31,19 @@ export type WebauthnAuthenticator = { createdAt: string; }; +export type AuthenticationSelection = { + authExecId: string; + authenticationExecution: { + alternative: boolean; + enabled: boolean; + authenticator: + | "auth-otp-form" + | "webauthn-authenticator" + | "auth-recovery-authn-code-form"; + priority: string; + }; +}; + export declare namespace KcContextBase { export type Common = { url: { @@ -327,13 +340,19 @@ export declare namespace KcContextBase { }; export type SAMLPostForm = Common & { - pageId: "saml-post-form.ftl" + pageId: "saml-post-form.ftl"; samlPost: { url: string; SAMLRequest?: string; SAMLResponse?: string; relayState?: string; - } + }; + }; + + export type SelectAuthenticator = Common & { + auth: { + authenticationSelections: Array; + }; }; } diff --git a/src/types/samlPostData.ts b/src/types/samlPostData.ts index edc3d0a..c945655 100644 --- a/src/types/samlPostData.ts +++ b/src/types/samlPostData.ts @@ -1,5 +1,5 @@ export type SAMLPostData = { - SAMLRequest?: string, - SAMLResponse?: string, - RelayState?: string, -} \ No newline at end of file + SAMLRequest?: string; + SAMLResponse?: string; + RelayState?: string; +}; diff --git a/src/views/saml-post-form/index.vue b/src/views/saml-post-form/index.vue index c260cae..ead43f7 100644 --- a/src/views/saml-post-form/index.vue +++ b/src/views/saml-post-form/index.vue @@ -2,7 +2,14 @@
- +

Redirecting...

@@ -12,7 +19,8 @@ import { defineComponent } from "vue"; import Layout from "~/components/Layout.vue"; import type { KcContextBase } from "~/types/context"; -import type { SAMLPostData } from "~/types/samlPostData" +import type { SAMLPostData } from "~/types/samlPostData"; +import { formPost } from "~/functions/utils"; export default defineComponent({ name: "SamlPostForm", @@ -30,31 +38,7 @@ export default defineComponent({ SAMLResponse: this.context.samlPost.SAMLResponse, RelayState: this.context.samlPost.relayState, }; - this.formPost(this.context.samlPost.url, postData); - }, - methods: { - formPost(url: string, data: object) { - const form = document.createElement("form"); - form.method = "post"; - form.action = url; - - for (const key in data) { - if (Object.prototype.hasOwnProperty.call(data, key)) { - if (data[key] === undefined) { - continue; - } - - const hiddenField = document.createElement("input"); - hiddenField.type = "hidden"; - hiddenField.name = key; - hiddenField.value = data[key]; - - form.appendChild(hiddenField); - } - } - document.body.appendChild(form); - form.submit(); - }, + formPost(this.context.samlPost.url, postData); }, }); diff --git a/src/views/select-authenticator/index.ftl b/src/views/select-authenticator/index.ftl new file mode 100644 index 0000000..c45b1f6 --- /dev/null +++ b/src/views/select-authenticator/index.ftl @@ -0,0 +1,6 @@ +<#import "baselayout.ftl" as layout> +<@layout.baseLayout pageId="select-authenticator.ftl" ; section> + <#if section = "scripts"> + + + diff --git a/src/views/select-authenticator/index.ts b/src/views/select-authenticator/index.ts new file mode 100644 index 0000000..9344ee4 --- /dev/null +++ b/src/views/select-authenticator/index.ts @@ -0,0 +1,7 @@ +import { createApp } from "vue"; +import index from "./index.vue"; + +if ((window as any).kcContext) { + const app = createApp(index); + app.mount("#app"); +} diff --git a/src/views/select-authenticator/index.vue b/src/views/select-authenticator/index.vue new file mode 100644 index 0000000..3364039 --- /dev/null +++ b/src/views/select-authenticator/index.vue @@ -0,0 +1,143 @@ + + + diff --git a/src/views/webauthn-authenticate/index.vue b/src/views/webauthn-authenticate/index.vue index ed6a2e8..d955d32 100644 --- a/src/views/webauthn-authenticate/index.vue +++ b/src/views/webauthn-authenticate/index.vue @@ -84,6 +84,7 @@ import Layout from "~/components/Layout.vue"; import ErrorBox from "~/components/ErrorBox.vue"; import type { KcContextBase } from "~/types/context"; import { base64url } from "rfc4648"; +import { formPost } from "~/functions/utils"; export default defineComponent({ name: "WebAuthnAuthenticate", @@ -106,7 +107,7 @@ export default defineComponent({ methods: { tryAnotherWay(e: Event) { e.preventDefault(); - this.formPost(this.context.url.loginAction, { + formPost(this.context.url.loginAction, { tryAnotherWay: "on", }); }, @@ -166,7 +167,7 @@ export default defineComponent({ ), }; - this.formPost(this.context.url.loginAction, postData); + formPost(this.context.url.loginAction, postData); } catch (err) { this.error = true; } @@ -174,24 +175,6 @@ export default defineComponent({ retryAuth() { this.error = false; }, - formPost(url: string, data: object) { - const form = document.createElement("form"); - form.method = "post"; - form.action = url; - - for (const key in data) { - if (Object.prototype.hasOwnProperty.call(data, key)) { - const hiddenField = document.createElement("input"); - hiddenField.type = "hidden"; - hiddenField.name = key; - hiddenField.value = data[key]; - - form.appendChild(hiddenField); - } - } - document.body.appendChild(form); - form.submit(); - }, }, }); diff --git a/webpack.config.js b/webpack.config.js index ba34e35..3ac5daf 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,7 +5,12 @@ const { CleanWebpackPlugin } = require("clean-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin"); const THEME_NAME = "modern-login"; -const customPages = ["login", "webauthn-authenticate", "saml-post-form"]; +const customPages = [ + "login", + "webauthn-authenticate", + "saml-post-form", + "select-authenticator" +]; module.exports = function (env, argv) { const isDevelopment = argv.mode !== "production";