diff --git a/server-spi-private/src/main/java/org/keycloak/forms/account/AccountPages.java b/server-spi-private/src/main/java/org/keycloak/forms/account/AccountPages.java index 6c01f79aba..a827f3e341 100755 --- a/server-spi-private/src/main/java/org/keycloak/forms/account/AccountPages.java +++ b/server-spi-private/src/main/java/org/keycloak/forms/account/AccountPages.java @@ -22,6 +22,6 @@ package org.keycloak.forms.account; */ public enum AccountPages { - ACCOUNT, PASSWORD, TOTP, FEDERATED_IDENTITY, LOG, SESSIONS, APPLICATIONS, RESOURCES, RESOURCE_DETAIL; + ACCOUNT, PASSWORD, AUTHENTICATION, FEDERATED_IDENTITY, LOG, SESSIONS, APPLICATIONS, RESOURCES, RESOURCE_DETAIL; } diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/FreeMarkerAccountProvider.java b/services/src/main/java/org/keycloak/forms/account/freemarker/FreeMarkerAccountProvider.java index bac9ed9aec..862605b156 100755 --- a/services/src/main/java/org/keycloak/forms/account/freemarker/FreeMarkerAccountProvider.java +++ b/services/src/main/java/org/keycloak/forms/account/freemarker/FreeMarkerAccountProvider.java @@ -20,18 +20,7 @@ import org.jboss.logging.Logger; import org.keycloak.events.Event; import org.keycloak.forms.account.AccountPages; import org.keycloak.forms.account.AccountProvider; -import org.keycloak.forms.account.freemarker.model.AccountBean; -import org.keycloak.forms.account.freemarker.model.AccountFederatedIdentityBean; -import org.keycloak.forms.account.freemarker.model.ApplicationsBean; -import org.keycloak.forms.account.freemarker.model.AuthorizationBean; -import org.keycloak.forms.account.freemarker.model.FeaturesBean; -import org.keycloak.forms.account.freemarker.model.LogBean; -import org.keycloak.forms.account.freemarker.model.PasswordBean; -import org.keycloak.forms.account.freemarker.model.RealmBean; -import org.keycloak.forms.account.freemarker.model.ReferrerBean; -import org.keycloak.forms.account.freemarker.model.SessionsBean; -import org.keycloak.forms.account.freemarker.model.TotpBean; -import org.keycloak.forms.account.freemarker.model.UrlBean; +import org.keycloak.forms.account.freemarker.model.*; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; @@ -164,8 +153,8 @@ public class FreeMarkerAccountProvider implements AccountProvider { attributes.put("account", new AccountBean(user, profileFormData)); switch (page) { - case TOTP: - attributes.put("totp", new TotpBean(session, realm, user, uriInfo.getRequestUriBuilder())); + case AUTHENTICATION: + attributes.put("authentication", new AuthenticationBean(session, realm, user, uriInfo)); break; case FEDERATED_IDENTITY: attributes.put("federatedIdentity", new AccountFederatedIdentityBean(session, realm, user, uriInfo.getBaseUri(), stateChecker)); diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/Templates.java b/services/src/main/java/org/keycloak/forms/account/freemarker/Templates.java index 48822937f3..ceb7744296 100755 --- a/services/src/main/java/org/keycloak/forms/account/freemarker/Templates.java +++ b/services/src/main/java/org/keycloak/forms/account/freemarker/Templates.java @@ -30,8 +30,8 @@ public class Templates { return "account.ftl"; case PASSWORD: return "password.ftl"; - case TOTP: - return "totp.ftl"; + case AUTHENTICATION: + return "authentication.ftl"; case FEDERATED_IDENTITY: return "federatedIdentity.ftl"; case LOG: diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/model/AuthenticationBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/AuthenticationBean.java new file mode 100755 index 0000000000..bf6458cf78 --- /dev/null +++ b/services/src/main/java/org/keycloak/forms/account/freemarker/model/AuthenticationBean.java @@ -0,0 +1,244 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.forms.account.freemarker.model; + +import org.keycloak.authentication.Authenticator; +import org.keycloak.authentication.AuthenticatorFactory; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.common.util.reflections.Types; +import org.keycloak.credential.*; +import org.keycloak.models.*; +import org.keycloak.models.utils.ModelToRepresentation; +import org.keycloak.representations.account.CredentialMetadataRepresentation; +import org.keycloak.representations.idm.CredentialRepresentation; + +import javax.ws.rs.core.UriInfo; +import java.util.*; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.keycloak.models.AuthenticationExecutionModel.Requirement.DISABLED; +import static org.keycloak.utils.CredentialHelper.createUserStorageCredentialRepresentation; + +/** + * @author Kayden Tebau + */ +public class AuthenticationBean { + + private final KeycloakSession session; + private final RealmModel realm; + private final UserModel user; + private final AuthorizationProvider authorization; + private final UriInfo uriInfo; + + private final Map credTypes; + + private final Map> credentials; + + public AuthenticationBean(KeycloakSession session, RealmModel realm, UserModel user, UriInfo uriInfo) { + this.session = session; + this.realm = realm; + this.user = user; + this.uriInfo = uriInfo; + authorization = session.getProvider(AuthorizationProvider.class); + credTypes = findCredentialTypes(); + credentials = new HashMap<>(); + } + + public static class CredentialUserMetadataBean { + + } + + public static class CredentialTypeBean { + // ** category, displayName and helptext attributes can be ordinary UI text or a key into + // a localized message bundle. Typically, it will be a key, but + // the UI will work just fine if you don't care about localization + // and you want to just send UI text. + // + // Also, the ${} shown in Apicurio is not needed. + private String type; + private String category; // ** + private String displayName; + private String helptext; // ** + private String iconCssClass; + private String createAction; + private String updateAction; + private boolean removable; + private List userCredentialMetadatas; + private CredentialTypeMetadata metadata; + + public CredentialTypeBean() { + } + + public CredentialTypeBean(CredentialTypeMetadata metadata, List userCredentialMetadatas) { + this.metadata = metadata; + this.type = metadata.getType(); + this.category = metadata.getCategory().toString(); + this.displayName = metadata.getDisplayName(); + this.helptext = metadata.getHelpText(); + this.iconCssClass = metadata.getIconCssClass(); + this.createAction = metadata.getCreateAction(); + this.updateAction = metadata.getUpdateAction(); + this.removable = metadata.isRemoveable(); + this.userCredentialMetadatas = userCredentialMetadatas; + } + + public String getCategory() { + return category; + } + + public String getType() { + return type; + } + + public String getDisplayName() { + return displayName; + } + + public String getHelpText() { + return helptext; + } + + public String getIconCssClass() { + return iconCssClass; + } + + public String getCreateAction() { + return createAction; + } + + public String getUpdateAction() { + return updateAction; + } + + public boolean getRemovable() { + return removable; + } + + public List getUserCredentialMetadatas() { + return userCredentialMetadatas; + } + + public CredentialTypeMetadata getMetadata() { + return metadata; + } + } + + public List getCredentialTypes() { + return new ArrayList<>(credTypes.values()); + } + + + private Map findCredentialTypes() { + List credentialProviders = session.getKeycloakSessionFactory().getProviderFactoriesStream(CredentialProvider.class) + .filter(f -> Types.supports(CredentialProvider.class, f, CredentialProviderFactory.class)) + .map(f -> session.getProvider(CredentialProvider.class, f.getId())) + .collect(Collectors.toList()); + Set enabledCredentialTypes = getEnabledCredentialTypes(credentialProviders); + + Stream modelsStream = user.credentialManager().getStoredCredentialsStream(); + List models = modelsStream.collect(Collectors.toList()); + + Function toCredentialBean = (credentialProvider) -> { + CredentialTypeMetadataContext ctx = CredentialTypeMetadataContext.builder() + .user(user) + .build(session); + CredentialTypeMetadata metadata = credentialProvider.getCredentialTypeMetadata(ctx); + + List userCredentialMetadataModels = null; + + List modelsOfType = models.stream() + .filter(credentialModel -> credentialProvider.getType().equals(credentialModel.getType())) + .collect(Collectors.toList()); + + + List credentialMetadataList = modelsOfType.stream() + .map(m -> { + return credentialProvider.getCredentialMetadata( + credentialProvider.getCredentialFromModel(m), metadata + ); + }).collect(Collectors.toList()); + + credentialMetadataList.stream().forEach(md -> md.getCredentialModel().setSecretData(null)); + userCredentialMetadataModels = credentialMetadataList.stream().map(ModelToRepresentation::toRepresentation).collect(Collectors.toList()); + + if (userCredentialMetadataModels.isEmpty() && + user.credentialManager().isConfiguredFor(credentialProvider.getType())) { + // In case user is federated in the userStorage, he may have credential configured on the userStorage side. We're + // creating "dummy" credential representing the credential provided by userStorage + CredentialMetadataRepresentation metadataRepresentation = new CredentialMetadataRepresentation(); + CredentialRepresentation credential = createUserStorageCredentialRepresentation(credentialProvider.getType()); + metadataRepresentation.setCredential(credential); + userCredentialMetadataModels = Collections.singletonList(metadataRepresentation); + } + + // In case that there are no userCredentials AND there are not required actions for setup new credential, + // we won't include credentialType as user won't be able to do anything with it + if (userCredentialMetadataModels.isEmpty() && metadata.getCreateAction() == null && metadata.getUpdateAction() == null) { + return null; + } + + return new CredentialTypeBean(metadata, userCredentialMetadataModels); + }; + + return credentialProviders.stream() + .filter(p -> enabledCredentialTypes.contains(p.getType())) + .map(toCredentialBean) + .filter(Objects::nonNull) + .sorted(Comparator.comparing(CredentialTypeBean::getMetadata)) + .collect(Collectors.toMap(CredentialTypeBean::getType, Function.identity())); + } + + private Set getEnabledCredentialTypes(List credentialProviders) { + Stream enabledCredentialTypes = realm.getAuthenticationFlowsStream() + .filter(((Predicate) this::isFlowEffectivelyDisabled).negate()) + .flatMap(flow -> + realm.getAuthenticationExecutionsStream(flow.getId()) + .filter(exe -> Objects.nonNull(exe.getAuthenticator()) && exe.getRequirement() != DISABLED) + .map(exe -> (AuthenticatorFactory) session.getKeycloakSessionFactory() + .getProviderFactory(Authenticator.class, exe.getAuthenticator())) + .filter(Objects::nonNull) + .map(AuthenticatorFactory::getReferenceCategory) + .filter(Objects::nonNull) + ); + + Set credentialTypes = credentialProviders.stream() + .map(CredentialProvider::getType) + .collect(Collectors.toSet()); + + return enabledCredentialTypes.filter(credentialTypes::contains).collect(Collectors.toSet()); + } + + // Returns true if flow is effectively disabled - either it's execution or some parent execution is disabled + private boolean isFlowEffectivelyDisabled(AuthenticationFlowModel flow) { + while (!flow.isTopLevel()) { + AuthenticationExecutionModel flowExecution = realm.getAuthenticationExecutionByFlowId(flow.getId()); + if (flowExecution == null) return false; // Can happen under some corner cases + if (DISABLED == flowExecution.getRequirement()) return true; + if (flowExecution.getParentFlow() == null) return false; + + // Check parent flow + flow = realm.getAuthenticationFlowById(flowExecution.getParentFlow()); + if (flow == null) return false; + } + + return false; + } +} diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/model/TotpBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/TotpBean.java deleted file mode 100644 index 096c73110f..0000000000 --- a/services/src/main/java/org/keycloak/forms/account/freemarker/model/TotpBean.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.forms.account.freemarker.model; - -import org.keycloak.authentication.otp.OTPApplicationProvider; -import org.keycloak.credential.CredentialModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.OTPPolicy; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.credential.OTPCredentialModel; -import org.keycloak.models.utils.HmacOTP; -import org.keycloak.models.utils.RepresentationToModel; -import org.keycloak.representations.idm.CredentialRepresentation; -import org.keycloak.utils.TotpUtils; - -import javax.ws.rs.core.UriBuilder; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -import static org.keycloak.utils.CredentialHelper.createUserStorageCredentialRepresentation; - - -/** - * @author Stian Thorgersen - */ -public class TotpBean { - - private final RealmModel realm; - private final String totpSecret; - private final String totpSecretEncoded; - private final String totpSecretQrCode; - private final boolean enabled; - private KeycloakSession session; - private final UriBuilder uriBuilder; - private final List otpCredentials; - private final List supportedApplications; - - public TotpBean(KeycloakSession session, RealmModel realm, UserModel user, UriBuilder uriBuilder) { - this.session = session; - this.uriBuilder = uriBuilder; - this.enabled = user.credentialManager().isConfiguredFor(OTPCredentialModel.TYPE); - if (enabled) { - List otpCredentials = user.credentialManager().getStoredCredentialsByTypeStream(OTPCredentialModel.TYPE).collect(Collectors.toList()); - - if (otpCredentials.isEmpty()) { - // Credential is configured on userStorage side. Create the "fake" credential similar like we do for the new account console - CredentialRepresentation credential = createUserStorageCredentialRepresentation(OTPCredentialModel.TYPE); - this.otpCredentials = Collections.singletonList(RepresentationToModel.toModel(credential)); - } else { - this.otpCredentials = otpCredentials; - } - } else { - this.otpCredentials = Collections.EMPTY_LIST; - } - - this.realm = realm; - this.totpSecret = HmacOTP.generateSecret(20); - this.totpSecretEncoded = TotpUtils.encode(totpSecret); - this.totpSecretQrCode = TotpUtils.qrCode(totpSecret, realm, user); - - OTPPolicy otpPolicy = realm.getOTPPolicy(); - this.supportedApplications = session.getAllProviders(OTPApplicationProvider.class).stream() - .filter(p -> p.supports(otpPolicy)) - .map(OTPApplicationProvider::getName) - .collect(Collectors.toList()); - } - - public boolean isEnabled() { - return enabled; - } - - public String getTotpSecret() { - return totpSecret; - } - - public String getTotpSecretEncoded() { - return totpSecretEncoded; - } - - public String getTotpSecretQrCode() { - return totpSecretQrCode; - } - - public String getManualUrl() { - return uriBuilder.replaceQueryParam("mode", "manual").build().toString(); - } - - public String getQrUrl() { - return uriBuilder.replaceQueryParam("mode", "qr").build().toString(); - } - - public OTPPolicy getPolicy() { - return realm.getOTPPolicy(); - } - - public List getSupportedApplications() { - return supportedApplications; - } - - public List getOtpCredentials() { - return otpCredentials; - } - -} - diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/model/UrlBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/UrlBean.java index 4562059af3..f4e7531ca4 100755 --- a/services/src/main/java/org/keycloak/forms/account/freemarker/model/UrlBean.java +++ b/services/src/main/java/org/keycloak/forms/account/freemarker/model/UrlBean.java @@ -63,8 +63,8 @@ public class UrlBean { return Urls.accountFederatedIdentityPage(baseQueryURI, realm).toString(); } - public String getTotpUrl() { - return Urls.accountTotpPage(baseQueryURI, realm).toString(); + public String getAuthenticationUrl() { + return Urls.accountAuthenticationPage(baseQueryURI, realm).toString(); } public String getLogUrl() { diff --git a/services/src/main/java/org/keycloak/services/Urls.java b/services/src/main/java/org/keycloak/services/Urls.java index 8381bf94b5..706c13a7e6 100755 --- a/services/src/main/java/org/keycloak/services/Urls.java +++ b/services/src/main/java/org/keycloak/services/Urls.java @@ -129,8 +129,8 @@ public class Urls { .build(realmName); } - public static URI accountTotpPage(URI baseUri, String realmName) { - return accountBase(baseUri).path(AccountFormService.class, "totpPage").build(realmName); + public static URI accountAuthenticationPage(URI baseUri, String realmName) { + return accountBase(baseUri).path(AccountFormService.class, "authenticationPage").build(realmName); } public static URI accountLogPage(URI baseUri, String realmName) { diff --git a/services/src/main/java/org/keycloak/services/resources/AbstractSecuredLocalService.java b/services/src/main/java/org/keycloak/services/resources/AbstractSecuredLocalService.java index 40ec7f4f9a..952cf89d50 100755 --- a/services/src/main/java/org/keycloak/services/resources/AbstractSecuredLocalService.java +++ b/services/src/main/java/org/keycloak/services/resources/AbstractSecuredLocalService.java @@ -131,6 +131,10 @@ public abstract class AbstractSecuredLocalService { protected abstract URI getBaseRedirectUri(); protected Response login(String path) { + return login(path, null); + } + + protected Response login(String path, String action) { OAuthRedirect oauth = new OAuthRedirect(); String authUrl = OIDCLoginProtocolService.authUrl(session.getContext().getUri()).build(realm.getName()).toString(); oauth.setAuthUrl(authUrl); @@ -158,7 +162,7 @@ public abstract class AbstractSecuredLocalService { URI accountUri = uriBuilder.build(realm.getName()); oauth.setStateCookiePath(accountUri.getRawPath()); - return oauth.redirect(session.getContext().getUri(), accountUri.toString()); + return oauth.redirect(session.getContext().getUri(), accountUri.toString(), action); } static class OAuthRedirect extends AbstractOAuthClient { @@ -170,6 +174,10 @@ public abstract class AbstractSecuredLocalService { } public Response redirect(UriInfo uriInfo, String redirectUri) { + return redirect(uriInfo, redirectUri, null); + } + + public Response redirect(UriInfo uriInfo, String redirectUri, String kcAction) { String state = getStateCode(); String scopeParam = TokenUtil.attachOIDCScope(scope); @@ -180,6 +188,10 @@ public abstract class AbstractSecuredLocalService { .queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE) .queryParam(OAuth2Constants.SCOPE, scopeParam); + if (kcAction != null) { + uriBuilder.queryParam("kc_action", kcAction); + } + URI url = uriBuilder.build(); NewCookie cookie = new NewCookie(getStateCookieName(), state, getStateCookiePath(uriInfo), null, null, -1, isSecure, true); diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java index eb0257c59c..7125b3d207 100755 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java @@ -30,6 +30,7 @@ import org.keycloak.common.Profile; import org.keycloak.common.util.Base64Url; import org.keycloak.common.util.Time; import org.keycloak.common.util.UriUtils; +import org.keycloak.credential.CredentialProvider; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.Event; @@ -48,14 +49,11 @@ import org.keycloak.models.ClientSessionContext; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelException; -import org.keycloak.models.OTPPolicy; import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; -import org.keycloak.models.credential.OTPCredentialModel; import org.keycloak.models.credential.PasswordCredentialModel; -import org.keycloak.models.utils.CredentialValidation; import org.keycloak.models.utils.FormMessage; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.utils.RedirectUtils; @@ -83,7 +81,6 @@ import org.keycloak.userprofile.UserProfile; import org.keycloak.userprofile.UserProfileProvider; import org.keycloak.userprofile.EventAuditingAttributeChangeListener; import org.keycloak.util.JsonSerialization; -import org.keycloak.utils.CredentialHelper; import javax.ws.rs.Consumes; import javax.ws.rs.FormParam; @@ -270,15 +267,15 @@ public class AccountFormService extends AbstractSecuredLocalService { return forwardToPage(null, AccountPages.ACCOUNT); } - public static UriBuilder totpUrl(UriBuilder base) { - return RealmsResource.accountUrl(base).path(AccountFormService.class, "totpPage"); + public static UriBuilder authenticationUrl(UriBuilder base) { + return RealmsResource.accountUrl(base).path(AccountFormService.class, "authenticationPage"); } - @Path("totp") + @Path("authentication") @GET - public Response totpPage() { + public Response authenticationPage() { account.setAttribute("mode", session.getContext().getUri().getQueryParameters().getFirst("mode")); - return forwardToPage("totp", AccountPages.TOTP); + return forwardToPage("authentication", AccountPages.AUTHENTICATION); } public static UriBuilder passwordUrl(UriBuilder base) { @@ -495,24 +492,22 @@ public class AccountFormService extends AbstractSecuredLocalService { * * @return */ - @Path("totp") + @Path("authentication") @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - public Response processTotpUpdate() { + public Response processAuthenticationUpdate() { MultivaluedMap formData = request.getDecodedFormParameters(); if (auth == null) { - return login("totp"); + return login("authentication"); } auth.require(AccountRoles.MANAGE_ACCOUNT); - account.setAttribute("mode", session.getContext().getUri().getQueryParameters().getFirst("mode")); - String action = formData.getFirst("submitAction"); if (action != null && action.equals("Cancel")) { setReferrerOnPage(); - return account.createResponse(AccountPages.TOTP); + return account.createResponse(AccountPages.AUTHENTICATION); } csrfCheck(formData); @@ -521,37 +516,37 @@ public class AccountFormService extends AbstractSecuredLocalService { if (action != null && action.equals("Delete")) { String credentialId = formData.getFirst("credentialId"); - if (credentialId == null) { + String credType = formData.getFirst("credentialType"); + if (credentialId == null || credType == null) { setReferrerOnPage(); - return account.setError(Status.OK, Messages.UNEXPECTED_ERROR_HANDLING_REQUEST).createResponse(AccountPages.TOTP); + return account.setError(Status.OK, Messages.UNEXPECTED_ERROR_HANDLING_REQUEST).createResponse(AccountPages.AUTHENTICATION); } - CredentialHelper.deleteOTPCredential(session, realm, user, credentialId); - event.event(EventType.REMOVE_TOTP).client(auth.getClient()).user(auth.getUser()).success(); + CredentialProvider credentialProvider = session.getProvider(CredentialProvider.class, credType); + if (credentialProvider == null) { + setReferrerOnPage(); + return account.setError(Status.OK, Messages.UNEXPECTED_ERROR_HANDLING_REQUEST).createResponse(AccountPages.AUTHENTICATION); + } + boolean removed = credentialProvider.deleteCredential(realm, user, credentialId); + if (!removed) { + setReferrerOnPage(); + return account.setError(Status.OK, Messages.UNEXPECTED_ERROR_HANDLING_REQUEST).createResponse(AccountPages.AUTHENTICATION); + } + + // TODO: event??? + // event.event(EventType.REMOVE_TOTP).client(auth.getClient()).user(auth.getUser()).success(); + + // TODO: string key setReferrerOnPage(); - return account.setSuccess(Messages.SUCCESS_TOTP_REMOVED).createResponse(AccountPages.TOTP); + return account.setSuccess(Messages.SUCCESS_TOTP_REMOVED).createResponse(AccountPages.AUTHENTICATION); } else { - String challengeResponse = formData.getFirst("totp"); - String totpSecret = formData.getFirst("totpSecret"); - String userLabel = formData.getFirst("userLabel"); - - OTPPolicy policy = realm.getOTPPolicy(); - OTPCredentialModel credentialModel = OTPCredentialModel.createFromPolicy(realm, totpSecret, userLabel); - if (Validation.isBlank(challengeResponse)) { + String kcAction = formData.getFirst("kcAction"); + if (kcAction == null) { setReferrerOnPage(); - return account.setError(Status.OK, Messages.MISSING_TOTP).createResponse(AccountPages.TOTP); - } else if (!CredentialValidation.validOTP(challengeResponse, credentialModel, policy.getLookAheadWindow())) { - setReferrerOnPage(); - return account.setError(Status.OK, Messages.INVALID_TOTP).createResponse(AccountPages.TOTP); + return account.setError(Status.OK, Messages.UNEXPECTED_ERROR_HANDLING_REQUEST).createResponse(AccountPages.AUTHENTICATION); } - if (!CredentialHelper.createOTPCredential(session, realm, user, challengeResponse, credentialModel)) { - setReferrerOnPage(); - return account.setError(Status.OK, Messages.INVALID_TOTP).createResponse(AccountPages.TOTP); - } - event.event(EventType.UPDATE_TOTP).client(auth.getClient()).user(auth.getUser()).success(); - - setReferrerOnPage(); - return account.setSuccess(Messages.SUCCESS_TOTP).createResponse(AccountPages.TOTP); + // Do action with kc_action param + return login("authentication", kcAction); } } diff --git a/themes/src/main/resources/META-INF/keycloak-themes.json b/themes/src/main/resources/META-INF/keycloak-themes.json index 1df68b3b51..60c22bf454 100755 --- a/themes/src/main/resources/META-INF/keycloak-themes.json +++ b/themes/src/main/resources/META-INF/keycloak-themes.json @@ -8,5 +8,8 @@ }, { "name" : "keycloak.v2", "types": [ "account", "admin" ] + }, { + "name" : "unixdog", + "types": [ "account", "login" ] }] } diff --git a/themes/src/main/resources/theme/unixdog/account/account.ftl b/themes/src/main/resources/theme/unixdog/account/account.ftl new file mode 100755 index 0000000000..f88833a034 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/account/account.ftl @@ -0,0 +1,38 @@ +<#import "template.ftl" as layout> +<@layout.mainLayout active='account' bodyClass='user'; section> +

${msg("editAccountHtmlTitle")}

+ * ${msg("requiredFields")} + +
+ + + + <#if !realm.registrationEmailAsUsername> +
+ <#if realm.editUsernameAllowed>* + disabled="disabled" value="${(account.username!'')}"/> +
+ + +
+ * + +
+ +
+ * + +
+ +
+ * + +
+ +
+ <#if url.referrerURI??>${kcSanitize(msg("backToApplication")?no_esc)} + + +
+
+ diff --git a/themes/src/main/resources/theme/unixdog/account/applications.ftl b/themes/src/main/resources/theme/unixdog/account/applications.ftl new file mode 100755 index 0000000000..3193a0ae6d --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/account/applications.ftl @@ -0,0 +1,72 @@ +<#import "template.ftl" as layout> +<@layout.mainLayout active='applications' bodyClass='applications'; section> + +

${msg("applicationsHtmlTitle")}

+ +
+ + + + + + + + + + + + + + + + <#list applications.applications as application> + + + + + + + + + + + + + +
${msg("application")}${msg("availableRoles")}${msg("grantedPermissions")}${msg("additionalGrants")}${msg("action")}
+ <#if application.effectiveUrl?has_content> + <#if application.client.name?has_content>${advancedMsg(application.client.name)}<#else>${application.client.clientId} + <#if application.effectiveUrl?has_content> + + <#list application.realmRolesAvailable as role> + <#if role.description??>${advancedMsg(role.description)}<#else>${advancedMsg(role.name)} + <#if role_has_next>, + + <#list application.resourceRolesAvailable?keys as resource> + <#if application.realmRolesAvailable?has_content>, + <#list application.resourceRolesAvailable[resource] as clientRole> + <#if clientRole.roleDescription??>${advancedMsg(clientRole.roleDescription)}<#else>${advancedMsg(clientRole.roleName)} + ${msg("inResource")} <#if clientRole.clientName??>${advancedMsg(clientRole.clientName)}<#else>${clientRole.clientId} + <#if clientRole_has_next>, + + + + <#if application.client.consentRequired> + <#list application.clientScopesGranted as claim> + ${advancedMsg(claim)}<#if claim_has_next>, + + <#else> + ${msg("fullAccess")} + + + <#list application.additionalGrants as grant> + ${advancedMsg(grant)}<#if grant_has_next>, + + + <#if (application.client.consentRequired && application.clientScopesGranted?has_content) || application.additionalGrants?has_content> + + +
+
+ + diff --git a/themes/src/main/resources/theme/unixdog/account/authentication.ftl b/themes/src/main/resources/theme/unixdog/account/authentication.ftl new file mode 100755 index 0000000000..347b157cfd --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/account/authentication.ftl @@ -0,0 +1,58 @@ +<#import "template.ftl" as layout> +<@layout.mainLayout active='authentication' bodyClass='authentication'; section> + +

${msg("authenticationTitle")}

+ + <#list authentication.credentialTypes as credType> +
+

${msg(credType.displayName)}

+

${msg(credType.helpText!"default-help-text")}

+ + + + + + + + + + <#list credType.userCredentialMetadatas as userData> + + + + + + + +
NameCreatedActions
${userData.credential.userLabel!"No label"}${userData.credential.createdDate?number_to_datetime!"No date"} + <#if credType.removable!false> +
+ + + + +
+ + <#if credType.updateAction??> +
+ + + + + +
+ +
+ <#if credType.createAction??> +
+ + + + +
+ +
+ + + + diff --git a/themes/src/main/resources/theme/unixdog/account/federatedIdentity.ftl b/themes/src/main/resources/theme/unixdog/account/federatedIdentity.ftl new file mode 100755 index 0000000000..c2eb76985f --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/account/federatedIdentity.ftl @@ -0,0 +1,42 @@ +<#import "template.ftl" as layout> +<@layout.mainLayout active='social' bodyClass='social'; section> + +
+
+

${msg("federatedIdentitiesHtmlTitle")}

+
+
+ +
+ <#list federatedIdentity.identities as identity> +
+
+ +
+
+ +
+
+ <#if identity.connected> + <#if federatedIdentity.removeLinkPossible> +
+ + + + +
+ + <#else> +
+ + + + +
+ +
+
+ +
+ + diff --git a/themes/src/main/resources/theme/unixdog/account/log.ftl b/themes/src/main/resources/theme/unixdog/account/log.ftl new file mode 100644 index 0000000000..29046cf0d2 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/account/log.ftl @@ -0,0 +1,35 @@ +<#import "template.ftl" as layout> +<@layout.mainLayout active='log' bodyClass='log'; section> + +
+
+

${msg("accountLogHtmlTitle")}

+
+
+ + + + + + + + + + + + + + <#list log.events as event> + + + + + + + + + + +
${msg("date")}${msg("event")}${msg("ip")}${msg("client")}${msg("details")}
${event.date?datetime}${event.event}${event.ipAddress}${event.client!}<#list event.details as detail>${detail.key} = ${detail.value} <#if detail_has_next>,
+ + \ No newline at end of file diff --git a/themes/src/main/resources/theme/unixdog/account/messages/messages_en.properties b/themes/src/main/resources/theme/unixdog/account/messages/messages_en.properties new file mode 100755 index 0000000000..d96e6bdc66 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/account/messages/messages_en.properties @@ -0,0 +1,441 @@ +doSave=Save +doCancel=Cancel +doLogOutAllSessions=Log out all sessions +doRemove=Remove +doAdd=Add +doSignOut=Sign out +doLogIn=Log In +doLink=Link +noAccessMessage=Access not allowed + +personalInfoSidebarTitle=Personal info +accountSecuritySidebarTitle=Account security +signingInSidebarTitle=Signing in +deviceActivitySidebarTitle=Device activity +linkedAccountsSidebarTitle=Linked accounts + +editAccountHtmlTitle=Edit Account +personalInfoHtmlTitle=Personal Info +federatedIdentitiesHtmlTitle=Federated Identities +accountLogHtmlTitle=Account Log +changePasswordHtmlTitle=Change Password +deviceActivityHtmlTitle=Device Activity +sessionsHtmlTitle=Sessions +accountManagementTitle=Keycloak Account Management +authenticatorTitle=Authenticator +applicationsHtmlTitle=Applications +linkedAccountsHtmlTitle=Linked accounts + +accountManagementWelcomeMessage=Welcome to Keycloak Account Management +personalInfoIntroMessage=Manage your basic information +accountSecurityTitle=Account Security +accountSecurityIntroMessage=Control your password and account access +applicationsIntroMessage=Track and manage your app permission to access your account +resourceIntroMessage=Share your resources among team members +passwordLastUpdateMessage=Your password was updated at +updatePasswordTitle=Update Password +updatePasswordMessageTitle=Make sure you choose a strong password +updatePasswordMessage=A strong password contains a mix of numbers, letters, and symbols. It is hard to guess, does not resemble a real word, and is only used for this account. +personalSubTitle=Your Personal Info +personalSubMessage=Manage your basic information. + +authenticatorCode=One-time code +email=Email +firstName=First name +givenName=Given name +fullName=Full name +lastName=Last name +familyName=Family name +password=Password +currentPassword=Current Password +passwordConfirm=Confirmation +passwordNew=New Password +username=Username +address=Address +street=Street +locality=City or Locality +region=State, Province, or Region +postal_code=Zip or Postal code +country=Country +emailVerified=Email verified +website=Web page +phoneNumber=Phone number +phoneNumberVerified=Phone number verified +gender=Gender +birthday=Birthdate +zoneinfo=Time zone +gssDelegationCredential=GSS Delegation Credential + +profileScopeConsentText=User profile +emailScopeConsentText=Email address +addressScopeConsentText=Address +phoneScopeConsentText=Phone number +offlineAccessScopeConsentText=Offline Access +samlRoleListScopeConsentText=My Roles +rolesScopeConsentText=User roles + +role_admin=Admin +role_realm-admin=Realm Admin +role_create-realm=Create realm +role_view-realm=View realm +role_view-users=View users +role_view-applications=View applications +role_view-groups=View groups +role_view-clients=View clients +role_view-events=View events +role_view-identity-providers=View identity providers +role_view-consent=View consents +role_manage-realm=Manage realm +role_manage-users=Manage users +role_manage-applications=Manage applications +role_manage-identity-providers=Manage identity providers +role_manage-clients=Manage clients +role_manage-events=Manage events +role_view-profile=View profile +role_manage-account=Manage account +role_manage-account-links=Manage account links +role_manage-consent=Manage consents +role_read-token=Read token +role_offline-access=Offline access +role_uma_authorization=Obtain permissions +client_account=Account +client_account-console=Account Console +client_security-admin-console=security admin console +client_admin-cli=Admin CLI +client_realm-management=Realm Management +client_broker=Broker + + +requiredFields=Required fields +allFieldsRequired=All fields required + +backToApplication=« Back to application +backTo=Back to {0} + +date=Date +event=Event +ip=IP +client=Client +clients=Clients +details=Details +started=Started +lastAccess=Last Access +expires=Expires +applications=Applications + +account=Account +federatedIdentity=Federated Identity +authenticator=Authenticator +device-activity=Device activity +sessions=Sessions +log=Log + +application=Application +availableRoles=Available Roles +grantedPermissions=Granted Permissions +grantedPersonalInfo=Granted Personal Info +additionalGrants=Additional Grants +action=Action +inResource=in +fullAccess=Full Access +offlineToken=Offline Token +revoke=Revoke Grant + +configureAuthenticators=Configured Authenticators +mobile=Mobile +totpStep1=Install one of the following applications on your mobile: +totpStep2=Open the application and scan the barcode: +totpStep3=Enter the one-time code provided by the application and click Save to finish the setup. +totpStep3DeviceName=Provide a Device Name to help you manage your OTP devices. + +totpManualStep2=Open the application and enter the key: +totpManualStep3=Use the following configuration values if the application allows setting them: +totpUnableToScan=Unable to scan? +totpScanBarcode=Scan barcode? + +totp.totp=Time-based +totp.hotp=Counter-based + +totpType=Type +totpAlgorithm=Algorithm +totpDigits=Digits +totpInterval=Interval +totpCounter=Counter +totpDeviceName=Device Name + +totpAppFreeOTPName=FreeOTP +totpAppGoogleName=Google Authenticator +totpAppMicrosoftAuthenticatorName=Microsoft Authenticator + +irreversibleAction=This action is irreversible +deletingImplies=Deleting your account implies: +errasingData=Erasing all your data +loggingOutImmediately=Logging you out immediately +accountUnusable=Any subsequent use of the application will not be possible with this account + +missingUsernameMessage=Please specify username. +missingFirstNameMessage=Please specify first name. +invalidEmailMessage=Invalid email address. +missingLastNameMessage=Please specify last name. +missingEmailMessage=Please specify email. +missingPasswordMessage=Please specify password. +notMatchPasswordMessage=Passwords don''t match. +invalidUserMessage=Invalid user +updateReadOnlyAttributesRejectedMessage=Update of read-only attribute rejected + +missingTotpMessage=Please specify authenticator code. +missingTotpDeviceNameMessage=Please specify device name. +invalidPasswordExistingMessage=Invalid existing password. +invalidPasswordConfirmMessage=Password confirmation doesn''t match. +invalidTotpMessage=Invalid authenticator code. + +usernameExistsMessage=Username already exists. +emailExistsMessage=Email already exists. + +readOnlyUserMessage=You can''t update your account as it is read-only. +readOnlyUsernameMessage=You can''t update your username as it is read-only. +readOnlyPasswordMessage=You can''t update your password as your account is read-only. + +successTotpMessage=Mobile authenticator configured. +successTotpRemovedMessage=Mobile authenticator removed. + +successGrantRevokedMessage=Grant revoked successfully. + +accountUpdatedMessage=Your account has been updated. +accountPasswordUpdatedMessage=Your password has been updated. + +missingIdentityProviderMessage=Identity provider not specified. +invalidFederatedIdentityActionMessage=Invalid or missing action. +identityProviderNotFoundMessage=Specified identity provider not found. +federatedIdentityLinkNotActiveMessage=This identity is not active anymore. +federatedIdentityRemovingLastProviderMessage=You can''t remove last federated identity as you don''t have a password. +identityProviderRedirectErrorMessage=Failed to redirect to identity provider. +identityProviderRemovedMessage=Identity provider removed successfully. +identityProviderAlreadyLinkedMessage=Federated identity returned by {0} is already linked to another user. +staleCodeAccountMessage=The page expired. Please try one more time. +consentDenied=Consent denied. +access-denied-when-idp-auth=Access denied when authenticating with {0} + +accountDisabledMessage=Account is disabled, contact your administrator. + +accountTemporarilyDisabledMessage=Account is temporarily disabled, contact your administrator or try again later. +invalidPasswordMinLengthMessage=Invalid password: minimum length {0}. +invalidPasswordMaxLengthMessage=Invalid password: maximum length {0}. +invalidPasswordMinLowerCaseCharsMessage=Invalid password: must contain at least {0} lower case characters. +invalidPasswordMinDigitsMessage=Invalid password: must contain at least {0} numerical digits. +invalidPasswordMinUpperCaseCharsMessage=Invalid password: must contain at least {0} upper case characters. +invalidPasswordMinSpecialCharsMessage=Invalid password: must contain at least {0} special characters. +invalidPasswordNotUsernameMessage=Invalid password: must not be equal to the username. +invalidPasswordNotEmailMessage=Invalid password: must not be equal to the email. +invalidPasswordRegexPatternMessage=Invalid password: fails to match regex pattern(s). +invalidPasswordHistoryMessage=Invalid password: must not be equal to any of last {0} passwords. +invalidPasswordBlacklistedMessage=Invalid password: password is blacklisted. +invalidPasswordGenericMessage=Invalid password: new password doesn''t match password policies. + +# Authorization +myResources=My Resources +myResourcesSub=My resources +doDeny=Deny +doRevoke=Revoke +doApprove=Approve +doRemoveSharing=Remove Sharing +doRemoveRequest=Remove Request +peopleAccessResource=People with access to this resource +resourceManagedPolicies=Permissions granting access to this resource +resourceNoPermissionsGrantingAccess=No permissions granting access to this resource +anyAction=Any action +description=Description +name=Name +scopes=Scopes +resource=Resource +user=User +peopleSharingThisResource=People sharing this resource +shareWithOthers=Share with others +needMyApproval=Need my approval +requestsWaitingApproval=Your requests waiting approval +icon=Icon +requestor=Requestor +owner=Owner +resourcesSharedWithMe=Resources shared with me +permissionRequestion=Permission Requestion +permission=Permission +shares=share(s) +notBeingShared=This resource is not being shared. +notHaveAnyResource=You don't have any resources +noResourcesSharedWithYou=There are no resources shared with you +havePermissionRequestsWaitingForApproval=You have {0} permission request(s) waiting for approval. +clickHereForDetails=Click here for details. +resourceIsNotBeingShared=The resource is not being shared + +locale_ar=\u0639\u0631\u0628\u064A +locale_ca=Catal\u00e0 +locale_cs=\u010Ce\u0161tina +locale_de=Deutsch +locale_en=English +locale_es=Espa\u00f1ol +locale_fr=Fran\u00e7ais +locale_hu=Magyar +locale_it=Italiano +locale_ja=\u65e5\u672c\u8a9e +locale_lt=Lietuvi\u0173 +locale_nl=Nederlands +locale_no=Norsk +locale_pl=Polski +locale_pt-BR=Portugu\u00eas (Brasil) +locale_ru=\u0420\u0443\u0441\u0441\u043a\u0438\u0439 +locale_sk=Sloven\u010dina +locale_sv=Svenska +locale_tr=T\u00FCrk\u00E7e +locale_zh-CN=\u4e2d\u6587\u7b80\u4f53 +locale_fi=Suomi + +# Applications +applicationName=Name +applicationType=Application Type +applicationInUse=In-use app only +clearAllFilter=Clear all filters +activeFilters=Active filters +filterByName=Filter By Name ... +allApps=All applications +internalApps=Internal applications +thirdpartyApps=Third-Party applications +appResults=Results +clientNotFoundMessage=Client not found. + +# Authentication +authentication=Authentication +authenticationTitle=Authentication + +# Linked account +authorizedProvider=Authorized Provider +authorizedProviderMessage=Authorized Providers linked with your account +identityProvider=Identity Provider +identityProviderMessage=To link your account with identity providers you have configured +socialLogin=Social Login +userDefined=User Defined +removeAccess=Remove Access +removeAccessMessage=You will need to grant access again, if you want to use this app account. + +#Authenticator +authenticatorStatusMessage=Two-factor authentication is currently +authenticatorFinishSetUpTitle=Your Two-Factor Authentication +authenticatorFinishSetUpMessage=Each time you sign in to your Keycloak account, you will be asked to provide a two-factor authentication code. +authenticatorSubTitle=Set Up Two-Factor Authentication +authenticatorSubMessage=To enhance the security of your account, enable at least one of the available two-factor authentication methods. +authenticatorMobileTitle=Mobile Authenticator +authenticatorMobileMessage=Use mobile Authenticator to get Verification codes as the two-factor authentication. +authenticatorMobileFinishSetUpMessage=The authenticator has been bound to your phone. +authenticatorActionSetup=Set up +authenticatorSMSTitle=SMS Code +authenticatorSMSMessage=Keycloak will send the Verification code to your phone as the two-factor authentication. +authenticatorSMSFinishSetUpMessage=Text messages are sent to +authenticatorDefaultStatus=Default +authenticatorChangePhone=Change Phone Number + +#Authenticator - Mobile Authenticator setup +authenticatorMobileSetupTitle=Mobile Authenticator Setup +smscodeIntroMessage=Enter your phone number and a verification code will be sent to your phone. +mobileSetupStep1=Install an authenticator application on your phone. The applications listed here are supported. +mobileSetupStep2=Open the application and scan the barcode: +mobileSetupStep3=Enter the one-time code provided by the application and click Save to finish the setup. +scanBarCode=Want to scan the barcode? +enterBarCode=Enter the one-time code +doCopy=Copy +doFinish=Finish + +#Authenticator - SMS Code setup +authenticatorSMSCodeSetupTitle=SMS Code Setup +chooseYourCountry=Choose your country +enterYourPhoneNumber=Enter your phone number +sendVerficationCode=Send Verification Code +enterYourVerficationCode=Enter your verification code + +#Authenticator - backup Code setup +authenticatorBackupCodesSetupTitle=Recovery Authentication Codes Setup +realmName=Realm +doDownload=Download +doPrint=Print +generateNewBackupCodes=Generate New Recovery Authentication Codes +backtoAuthenticatorPage=Back to Authenticator Page + + +#Resources +resources=Resources +sharedwithMe=Shared with Me +share=Share +sharedwith=Shared with +accessPermissions=Access Permissions +permissionRequests=Permission Requests +approve=Approve +approveAll=Approve all +people=people +perPage=per page +currentPage=Current Page +sharetheResource=Share the resource +group=Group +selectPermission=Select Permission +addPeople=Add people to share your resource with +addTeam=Add team to share your resource with +myPermissions=My Permissions +waitingforApproval=Waiting for approval +anyPermission=Any Permission + +# Openshift messages +openshift.scope.user_info=User information +openshift.scope.user_check-access=User access information +openshift.scope.user_full=Full Access +openshift.scope.list-projects=List projects + +error-invalid-value=Invalid value. +error-invalid-blank=Please specify value. +error-empty=Please specify value. +error-invalid-length=Attribute {0} must have a length between {1} and {2}. +error-invalid-length-too-short=Attribute {0} must have minimal length of {1}. +error-invalid-length-too-long=Attribute {0} must have maximal length of {2}. +error-invalid-email=Invalid email address. +error-invalid-number=Invalid number. +error-number-out-of-range=Attribute {0} must be a number between {1} and {2}. +error-number-out-of-range-too-small=Attribute {0} must have minimal value of {1}. +error-number-out-of-range-too-big=Attribute {0} must have maximal value of {2}. +error-pattern-no-match=Invalid value. +error-invalid-uri=Invalid URL. +error-invalid-uri-scheme=Invalid URL scheme. +error-invalid-uri-fragment=Invalid URL fragment. +error-user-attribute-required=Please specify attribute {0}. +error-invalid-date=Invalid date. +error-user-attribute-read-only=The field {0} is read only. +error-username-invalid-character=Username contains invalid character. +error-person-name-invalid-character=Name contains invalid character. + +# Signing in page +signingIn=Signing in +signingInSubMessage=Configure ways to sign in. +credentialCreatedAt=Created +successRemovedMessage={0} was removed. +stopUsingCred=Stop using {0}? +changePassword=Change password +removeCred=Remove {0} +setUpNew=Set up {0} +removeCredAriaLabel=Remove credential +updateCredAriaLabel=Update credential +notSetUp={0} is not set up. +two-factor=Two-factor authentication +passwordless=Passwordless +unknown=Unknown +password-display-name=Password +password-help-text=Sign in by entering your password. +otp-display-name=OTP Codes (2FA) +otp-help-text=Enter a verification code from authenticator application. +default-help-text= +recovery-authn-code=My recovery authentication codes +recovery-authn-codes-display-name=Recovery authentication codes +recovery-authn-codes-help-text=These codes can be used to regain your access in case your other 2FA means are not available. +recovery-codes-number-used={0} recovery codes used +recovery-codes-number-remaining={0} recovery codes remaining +recovery-codes-generate-new-codes=Generate new codes to ensure access to your account +webauthn-display-name=WebAuthn (2FA) +webauthn-help-text=Use your security key to sign in. +webauthn-passwordless-display-name=Passwordless WebAuthn +webauthn-passwordless-help-text=Use your security key for passwordless sign in. +basic-authentication=Basic authentication +invalidRequestMessage=Invalid request diff --git a/themes/src/main/resources/theme/unixdog/account/password.ftl b/themes/src/main/resources/theme/unixdog/account/password.ftl new file mode 100755 index 0000000000..4a043f28b9 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/account/password.ftl @@ -0,0 +1,59 @@ +<#import "template.ftl" as layout> +<@layout.mainLayout active='password' bodyClass='password'; section> + +
+
+

${msg("changePasswordHtmlTitle")}

+
+
+ ${msg("allFieldsRequired")} +
+
+ +
+ + + <#if password.passwordSet> +
+
+ +
+ +
+ +
+
+ + + + +
+
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+
+ +
+
+
+
+ + diff --git a/themes/src/main/resources/theme/unixdog/account/resource-detail.ftl b/themes/src/main/resources/theme/unixdog/account/resource-detail.ftl new file mode 100755 index 0000000000..2c963d7732 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/account/resource-detail.ftl @@ -0,0 +1,277 @@ +<#import "template.ftl" as layout> +<@layout.mainLayout active='authorization' bodyClass='authorization'; section> + + + + +
+
+

+ ${msg("myResources")} <#if authorization.resource.displayName??>${authorization.resource.displayName}<#else>${authorization.resource.name} +

+
+
+ + <#if authorization.resource.iconUri??> + +
+ + +
+
+

+ ${msg("peopleAccessResource")} +

+
+
+
+
+ + + + + + + + + + + <#if authorization.resource.shares?size != 0> + <#list authorization.resource.shares as permission> + + + + + + + + + + + + + <#else> + + + + + +
${msg("user")}${msg("permission")}${msg("date")}${msg("action")}
+ <#if permission.requester.email??>${permission.requester.email}<#else>${permission.requester.username} + + <#if permission.scopes?size != 0> + <#list permission.scopes as scope> + <#if scope.granted && scope.scope??> + + <#else> + ${msg("anyPermission")} + + + <#else> + Any action + + + ${permission.createdDate?datetime} + + ${msg("doRevoke")} +
${msg("resourceIsNotBeingShared")}
+ +
+
+
+
+

+ ${msg("resourceManagedPolicies")} +

+
+
+
+
+ + + + + + + + + + <#if authorization.resource.policies?size != 0> + <#list authorization.resource.policies as permission> + + + + + + + + + + + + <#else> + + + + + +
${msg("description")}${msg("permission")}${msg("action")}
+ <#if permission.description??> + ${permission.description} + + + <#if permission.scopes?size != 0> + <#list permission.scopes as scope> + + + <#else> + ${msg("anyAction")} + + + ${msg("doRevoke")} +
+ ${msg("resourceNoPermissionsGrantingAccess")} +
+ +
+
+
+
+

+ ${msg("shareWithOthers")} +

+
+
+
+
+
+ +
+ * +
+
+
+
+ +
+
+
+ <#list authorization.resource.scopes as scope> + + +
+ +
+
+
+
+
+
+ diff --git a/themes/src/main/resources/theme/unixdog/account/resources.ftl b/themes/src/main/resources/theme/unixdog/account/resources.ftl new file mode 100755 index 0000000000..d86e8bc323 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/account/resources.ftl @@ -0,0 +1,403 @@ +<#import "template.ftl" as layout> +<@layout.mainLayout active='authorization' bodyClass='authorization'; section> + + +
+
+

+ ${msg("myResources")} +

+
+
+ + <#if authorization.resourcesWaitingApproval?size != 0> +
+
+

+ ${msg("needMyApproval")} +

+
+
+
+
+ + + + + + + + + + + <#list authorization.resourcesWaitingApproval as resource> + <#list resource.permissions as permission> + + + + + + + + + + + + + + +
${msg("resource")}${msg("requestor")}${msg("permissionRequestion")}${msg("action")}
+ <#if resource.displayName??>${resource.displayName}<#else>${resource.name} + + <#if permission.requester.email??>${permission.requester.email}<#else>${permission.requester.username} + + <#list permission.scopes as scope> + <#if scope.scope??> + + <#else> + ${msg("anyPermission")} + + + + ${msg("doApprove")} + ${msg("doDeny")} +
+
+
+ + +
+
+

+ ${msg("myResourcesSub")} +

+
+
+
+
+ + + + + + + + + + + <#if authorization.resources?size != 0> + <#list authorization.resources as resource> + + + + + + + <#else> + + + + + +
${msg("resource")}${msg("application")}${msg("peopleSharingThisResource")}
+ + <#if resource.displayName??>${resource.displayName}<#else>${resource.name} + + + <#if resource.resourceServer.baseUri??> + ${resource.resourceServer.name} + <#else> + ${resource.resourceServer.name} + + + <#if resource.shares?size != 0> + ${resource.shares?size} + <#else> + ${msg("notBeingShared")} + +
${msg("notHaveAnyResource")}
+
+
+ +
+
+

+ ${msg("resourcesSharedWithMe")} +

+
+
+
+
+
+ + + + + + + + + + + + + + <#if authorization.sharedResources?size != 0> + <#list authorization.sharedResources as resource> + + + + + + + + + + <#else> + + + + + +
disabled="true" + ${msg("resource")}${msg("owner")}${msg("application")}${msg("permission")}${msg("date")}
+ + + <#if resource.displayName??>${resource.displayName}<#else>${resource.name} + + ${resource.ownerName} + + <#if resource.resourceServer.baseUri??> + ${resource.resourceServer.name} + <#else> + ${resource.resourceServer.name} + + + <#if resource.permissions?size != 0> +
    + <#list resource.permissions as permission> + <#list permission.scopes as scope> + <#if scope.granted && scope.scope??> +
  • + <#if scope.scope.displayName??> + ${scope.scope.displayName} + <#else> + ${scope.scope.name} + +
  • + <#else> + ${msg("anyPermission")} + + + +
+ <#else> + Any action + +
+ ${resource.permissions[0].grantedDate?datetime} +
${msg("noResourcesSharedWithYou")}
+
+
+ <#if authorization.sharedResources?size != 0> + + +
+ + <#if authorization.resourcesWaitingOthersApproval?size != 0> +
+
+
+

+ ${msg("requestsWaitingApproval")} +

+
+
+
+
+ ${msg("havePermissionRequestsWaitingForApproval",authorization.resourcesWaitingOthersApproval?size)} + ${msg("clickHereForDetails")} +
+
+
+
+
+
+
+
+
+ +
+
+ + + \ No newline at end of file diff --git a/themes/src/main/resources/theme/unixdog/account/resources/css/main.css b/themes/src/main/resources/theme/unixdog/account/resources/css/main.css new file mode 100644 index 0000000000..5b800ef012 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/account/resources/css/main.css @@ -0,0 +1,218 @@ +body { + font-family: monospace; + margin: 0; + background: #080e08; + color: #f6f6f6; + + display: flex; + flex-direction: column; + min-height: 100vh; +} + +h1, h2 { + color: #4af626; +} + +footer { + padding: 10px; + max-width: 800px; + width: 100%; + background: #3a7920; + color: #f6f6f6; + + margin: auto auto 0; + box-sizing: border-box; +} + +a:link { + color: #2cacb0; +} + +a:visited { + color: #b4778f; +} + +tr:nth-child(even) td { + background-color: #222; +} + +@media (prefers-color-scheme: light) { + body { + background: #fcfffc; + color: #211c1b; + } + + h1, h2 { + color: #4c982a; + } + + a:link { + color: #1f7b7e; + } + + a:visited { + color: #7c5263; + } + + textarea { + background: #ffffff; + color: #211c1b; + } + + tr:nth-child(even) td { + background-color: #eee; + } +} + +header { + background: linear-gradient( + 90deg, + rgba(255, 0, 0, 1) 0%, + rgba(255, 154, 0, 1) 10%, + rgba(208, 222, 33, 1) 20%, + rgba(79, 220, 74, 1) 30%, + rgba(63, 218, 216, 1) 40%, + rgba(47, 201, 226, 1) 50%, + rgba(28, 127, 238, 1) 60%, + rgba(95, 21, 242, 1) 70%, + rgba(186, 12, 248, 1) 80%, + rgba(251, 7, 217, 1) 90%, + rgba(255, 0, 0, 1) 100% + ); + min-height: 50px; + display: flex; + color: black; + box-sizing: border-box; +} + +#header-content { + max-width: 800px; + margin: auto; + width: 100%; + align-content: center; + flex-direction: row; + display: flex; + padding: 10px; +} +#header-content h1 { + color: black; + margin: auto auto auto 1rem; +} + +#header-content img { + display: inline; +} + +main { + max-width: 800px; + margin: 0 auto; + padding: 10px; + width: 100%; + box-sizing: border-box; +} + + +span.copyleft { + display: inline-block; + transform: rotate(180deg); +} + +footer a:link, footer a:visited { + text-decoration: none; + font-weight: bold; + color: white; +} + +footer a:hover { + text-decoration: underline; +} + + +input { + background: #332c29; + color: #f6f6f6; +} + +textarea { + background: #332c29; + color: #f6f6f6; +} + +textarea.big { + width: 100%; + height: 5em; +} + +.error, .required, .alert-error, .form-group.has-error { + color: #f00; +} + +#nav { + margin: auto 0 auto auto; + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +#nav a { + display: block; + padding: 10px; +} + +pre { + white-space: pre-wrap; +} + +#header-content a:link { + text-decoration: none; + color: black; +} +#header-content a:visited { + text-decoration: none; + color: black; +} + +#header-content a:hover { + text-decoration: underline; +} + +label.field-with-error { + color: red; +} + +img.stats { + width: 100%; + display: block; +} + +/* Tables */ +table { + border-collapse: collapse; + width: 100%; +} +th, td { + padding: 5px; +} +th { + border-bottom: solid 2px currentColor; +} + +td:first-child { + border-right: solid 2px currentColor; + border-left: none; +} +td { + border-right: solid 2px currentColor; + border-left: solid 2px currentColor; +} +td:last-child { + border-right: none; + border-left: solid 2px currentColor; +} + +/* Responsive */ +.responsive-wrapper { + overflow-x: auto; + width: 100%; +} + diff --git a/themes/src/main/resources/theme/unixdog/account/resources/img/favicon.ico b/themes/src/main/resources/theme/unixdog/account/resources/img/favicon.ico new file mode 100644 index 0000000000..46eafce9bc Binary files /dev/null and b/themes/src/main/resources/theme/unixdog/account/resources/img/favicon.ico differ diff --git a/themes/src/main/resources/theme/unixdog/account/resources/js/base64url.js b/themes/src/main/resources/theme/unixdog/account/resources/js/base64url.js new file mode 100644 index 0000000000..64555bfbd5 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/account/resources/js/base64url.js @@ -0,0 +1,114 @@ +// for embedded scripts, quoted and modified from https://github.com/swansontec/rfc4648.js by William Swanson +'use strict'; +var base64url = base64url || {}; +(function(base64url) { + + function parse (string, encoding, opts = {}) { + // Build the character lookup table: + if (!encoding.codes) { + encoding.codes = {}; + for (let i = 0; i < encoding.chars.length; ++i) { + encoding.codes[encoding.chars[i]] = i; + } + } + + // The string must have a whole number of bytes: + if (!opts.loose && (string.length * encoding.bits) & 7) { + throw new SyntaxError('Invalid padding'); + } + + // Count the padding bytes: + let end = string.length; + while (string[end - 1] === '=') { + --end; + + // If we get a whole number of bytes, there is too much padding: + if (!opts.loose && !(((string.length - end) * encoding.bits) & 7)) { + throw new SyntaxError('Invalid padding'); + } + } + + // Allocate the output: + const out = new (opts.out || Uint8Array)(((end * encoding.bits) / 8) | 0); + + // Parse the data: + let bits = 0; // Number of bits currently in the buffer + let buffer = 0; // Bits waiting to be written out, MSB first + let written = 0; // Next byte to write + for (let i = 0; i < end; ++i) { + // Read one character from the string: + const value = encoding.codes[string[i]]; + if (value === void 0) { + throw new SyntaxError('Invalid character ' + string[i]); + } + + // Append the bits to the buffer: + buffer = (buffer << encoding.bits) | value; + bits += encoding.bits; + + // Write out some bits if the buffer has a byte's worth: + if (bits >= 8) { + bits -= 8; + out[written++] = 0xff & (buffer >> bits); + } + } + + // Verify that we have received just enough bits: + if (bits >= encoding.bits || 0xff & (buffer << (8 - bits))) { + throw new SyntaxError('Unexpected end of data'); + } + + return out + } + + function stringify (data, encoding, opts = {}) { + const { pad = true } = opts; + const mask = (1 << encoding.bits) - 1; + let out = ''; + + let bits = 0; // Number of bits currently in the buffer + let buffer = 0; // Bits waiting to be written out, MSB first + for (let i = 0; i < data.length; ++i) { + // Slurp data into the buffer: + buffer = (buffer << 8) | (0xff & data[i]); + bits += 8; + + // Write out as much as we can: + while (bits > encoding.bits) { + bits -= encoding.bits; + out += encoding.chars[mask & (buffer >> bits)]; + } + } + + // Partial character: + if (bits) { + out += encoding.chars[mask & (buffer << (encoding.bits - bits))]; + } + + // Add padding characters until we hit a byte boundary: + if (pad) { + while ((out.length * encoding.bits) & 7) { + out += '='; + } + } + + return out + } + + const encoding = { + chars: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_', + bits: 6 + } + + base64url.decode = function (string, opts) { + return parse(string, encoding, opts); + } + + base64url.encode = function (data, opts) { + return stringify(data, encoding, opts) + } + + return base64url; +}(base64url)); + + diff --git a/themes/src/main/resources/theme/unixdog/account/sessions.ftl b/themes/src/main/resources/theme/unixdog/account/sessions.ftl new file mode 100755 index 0000000000..ad242b8ca9 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/account/sessions.ftl @@ -0,0 +1,39 @@ +<#import "template.ftl" as layout> +<@layout.mainLayout active='sessions' bodyClass='sessions'; section> +

${msg("sessionsHtmlTitle")}

+ + + + + + + + + + + + + + <#list sessions.sessions as session> + + + + + + + + + + +
${msg("ip")}${msg("started")}${msg("lastAccess")}${msg("expires")}${msg("clients")}
${session.ipAddress}${session.started?datetime}${session.lastAccess?datetime}${session.expires?datetime} + <#list session.clients as client> + ${client}
+ +
+ +
+ + +
+ diff --git a/themes/src/main/resources/theme/unixdog/account/template.ftl b/themes/src/main/resources/theme/unixdog/account/template.ftl new file mode 100644 index 0000000000..fbe2552b25 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/account/template.ftl @@ -0,0 +1,68 @@ +<#macro mainLayout active bodyClass> + + + + + + + + ${msg("accountManagementTitle")} + + <#if properties.stylesCommon?has_content> + <#list properties.stylesCommon?split(' ') as style> + + + + <#if properties.styles?has_content> + <#list properties.styles?split(' ') as style> + + + + <#if properties.scripts?has_content> + <#list properties.scripts?split(' ') as script> + + + + + +
+
+ UNIX.dog +

UNIX.dog Account

+ +
+
+ +
+ <#if message?has_content> +
+

+ ${kcSanitize(message.summary)?no_esc} +

+
+ +
+ <#nested "content"> +
+
+ + + + diff --git a/themes/src/main/resources/theme/unixdog/account/theme.properties b/themes/src/main/resources/theme/unixdog/account/theme.properties new file mode 100644 index 0000000000..4a293845f0 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/account/theme.properties @@ -0,0 +1,4 @@ + +styles=css/main.css +kcInputErrorMessageClass=error + diff --git a/themes/src/main/resources/theme/unixdog/login/cli_splash.ftl b/themes/src/main/resources/theme/unixdog/login/cli_splash.ftl new file mode 100644 index 0000000000..cd9ebbb7a2 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/cli_splash.ftl @@ -0,0 +1,7 @@ + _ __ _ _ +| |/ /___ _ _ ___| | ___ __ _| | __ +| ' // _ \ | | |/ __| |/ _ \ / _` | |/ / +| . \ __/ |_| | (__| | (_) | (_| | < +|_|\_\___|\__, |\___|_|\___/ \__,_|_|\_\ + |___/ + diff --git a/themes/src/main/resources/theme/unixdog/login/code.ftl b/themes/src/main/resources/theme/unixdog/login/code.ftl new file mode 100755 index 0000000000..bb0621db93 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/code.ftl @@ -0,0 +1,19 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout; section> + <#if section = "header"> + <#if code.success> + ${msg("codeSuccessTitle")} + <#else> + ${kcSanitize(msg("codeErrorTitle", code.error))} + + <#elseif section = "form"> +
+ <#if code.success> +

${msg("copyCodeInstruction")}

+ + <#else> +

${kcSanitize(code.error)}

+ +
+ + diff --git a/themes/src/main/resources/theme/unixdog/login/delete-account-confirm.ftl b/themes/src/main/resources/theme/unixdog/login/delete-account-confirm.ftl new file mode 100644 index 0000000000..6aa93f0964 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/delete-account-confirm.ftl @@ -0,0 +1,33 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout; section> + + <#if section = "header"> + ${msg("deleteAccountConfirm")} + + <#elseif section = "form"> + +
+ +
+ + ${msg("irreversibleAction")} +
+ +

${msg("deletingImplies")}

+
    +
  • ${msg("loggingOutImmediately")}
  • +
  • ${msg("errasingData")}
  • +
+ + + +
+ + <#if triggered_from_aia> + + +
+
+ + + \ No newline at end of file diff --git a/themes/src/main/resources/theme/unixdog/login/error.ftl b/themes/src/main/resources/theme/unixdog/login/error.ftl new file mode 100755 index 0000000000..112ded3eae --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/error.ftl @@ -0,0 +1,16 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=false; section> + <#if section = "header"> + ${kcSanitize(msg("errorTitle"))?no_esc} + <#elseif section = "form"> +
+

${kcSanitize(message.summary)?no_esc}

+ <#if skipLink??> + <#else> + <#if client?? && client.baseUrl?has_content> +

${kcSanitize(msg("backToApplication"))?no_esc}

+ + +
+ + \ No newline at end of file diff --git a/themes/src/main/resources/theme/unixdog/login/frontchannel-logout.ftl b/themes/src/main/resources/theme/unixdog/login/frontchannel-logout.ftl new file mode 100644 index 0000000000..3de7d2500e --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/frontchannel-logout.ftl @@ -0,0 +1,30 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout; section> + <#if section = "header"> + + ${msg("frontchannel-logout.title")} + <#elseif section = "form"> +

${msg("frontchannel-logout.message")}

+
    + <#list logout.clients as client> +
  • + ${client.name} + +
  • + +
+ <#if logout.logoutRedirectUri?has_content> + + ${msg("doContinue")} + + + diff --git a/themes/src/main/resources/theme/unixdog/login/idp-review-user-profile.ftl b/themes/src/main/resources/theme/unixdog/login/idp-review-user-profile.ftl new file mode 100644 index 0000000000..1b70aeccb9 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/idp-review-user-profile.ftl @@ -0,0 +1,23 @@ +<#import "template.ftl" as layout> +<#import "user-profile-commons.ftl" as userProfileCommons> +<@layout.registrationLayout displayMessage=messagesPerField.exists('global') displayRequiredFields=true; section> + <#if section = "header"> + ${msg("loginIdpReviewProfileTitle")} + <#elseif section = "form"> +
+ + <@userProfileCommons.userProfileFormFields/> + +
+
+
+
+
+ +
+ +
+
+
+ + \ No newline at end of file diff --git a/themes/src/main/resources/theme/unixdog/login/info.ftl b/themes/src/main/resources/theme/unixdog/login/info.ftl new file mode 100755 index 0000000000..400e8bfdff --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/info.ftl @@ -0,0 +1,24 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=false; section> + <#if section = "header"> + <#if messageHeader??> + ${messageHeader} + <#else> + ${message.summary} + + <#elseif section = "form"> +
+

${message.summary}<#if requiredActions??><#list requiredActions>: <#items as reqActionItem>${kcSanitize(msg("requiredAction.${reqActionItem}"))?no_esc}<#sep>, <#else>

+ <#if skipLink??> + <#else> + <#if pageRedirectUri?has_content> +

${kcSanitize(msg("backToApplication"))?no_esc}

+ <#elseif actionUri?has_content> +

${kcSanitize(msg("proceedWithAction"))?no_esc}

+ <#elseif (client.baseUrl)?has_content> +

${kcSanitize(msg("backToApplication"))?no_esc}

+ + +
+ + \ No newline at end of file diff --git a/themes/src/main/resources/theme/unixdog/login/login-config-totp.ftl b/themes/src/main/resources/theme/unixdog/login/login-config-totp.ftl new file mode 100755 index 0000000000..80145856d8 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/login-config-totp.ftl @@ -0,0 +1,108 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayRequiredFields=false displayMessage=!messagesPerField.existsError('totp','userLabel'); section> + + <#if section = "header"> + ${msg("loginTotpTitle")} + <#elseif section = "form"> +
    +
  1. +

    ${msg("loginTotpStep1")}

    + +
      + <#list totp.supportedApplications as app> +
    • ${msg(app)}
    • + +
    +
  2. + + <#if mode?? && mode = "manual"> +
  3. +

    ${msg("loginTotpManualStep2")}

    +

    ${totp.totpSecretEncoded}

    +

    ${msg("loginTotpScanBarcode")}

    +
  4. +
  5. +

    ${msg("loginTotpManualStep3")}

    +

    +

      +
    • ${msg("loginTotpType")}: ${msg("loginTotp." + totp.policy.type)}
    • +
    • ${msg("loginTotpAlgorithm")}: ${totp.policy.getAlgorithmKey()}
    • +
    • ${msg("loginTotpDigits")}: ${totp.policy.digits}
    • + <#if totp.policy.type = "totp"> +
    • ${msg("loginTotpInterval")}: ${totp.policy.period}
    • + <#elseif totp.policy.type = "hotp"> +
    • ${msg("loginTotpCounter")}: ${totp.policy.initialCounter}
    • + +
    +

    +
  6. + <#else> +
  7. +

    ${msg("loginTotpStep2")}

    + Figure: Barcode
    +

    ${msg("loginTotpUnableToScan")}

    +
  8. + +
  9. +

    ${msg("loginTotpStep3")}

    +

    ${msg("loginTotpStep3DeviceName")}

    +
  10. +
+ +
+
+
+ * +
+
+ + + <#if messagesPerField.existsError('totp')> + + ${kcSanitize(messagesPerField.get('totp'))?no_esc} + + + +
+ + <#if mode??> +
+ +
+
+ <#if totp.otpCredentials?size gte 1>* +
+ +
+ + + <#if messagesPerField.existsError('userLabel')> + + ${kcSanitize(messagesPerField.get('userLabel'))?no_esc} + + +
+
+ + <#if isAppInitiatedAction??> + + + <#else> + + +
+ + \ No newline at end of file diff --git a/themes/src/main/resources/theme/unixdog/login/login-idp-link-confirm.ftl b/themes/src/main/resources/theme/unixdog/login/login-idp-link-confirm.ftl new file mode 100644 index 0000000000..c3537c5d3e --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/login-idp-link-confirm.ftl @@ -0,0 +1,13 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout; section> + <#if section = "header"> + ${msg("confirmLinkIdpTitle")} + <#elseif section = "form"> +
+
+ + +
+
+ + diff --git a/themes/src/main/resources/theme/unixdog/login/login-idp-link-email.ftl b/themes/src/main/resources/theme/unixdog/login/login-idp-link-email.ftl new file mode 100644 index 0000000000..0020178f06 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/login-idp-link-email.ftl @@ -0,0 +1,16 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout; section> + <#if section = "header"> + ${msg("emailLinkIdpTitle", idpDisplayName)} + <#elseif section = "form"> +

+ ${msg("emailLinkIdp1", idpDisplayName, brokerContext.username, realm.displayName)} +

+

+ ${msg("emailLinkIdp2")} ${msg("doClickHere")} ${msg("emailLinkIdp3")} +

+

+ ${msg("emailLinkIdp4")} ${msg("doClickHere")} ${msg("emailLinkIdp5")} +

+ + \ No newline at end of file diff --git a/themes/src/main/resources/theme/unixdog/login/login-oauth-grant.ftl b/themes/src/main/resources/theme/unixdog/login/login-oauth-grant.ftl new file mode 100755 index 0000000000..d5cfc4aac7 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/login-oauth-grant.ftl @@ -0,0 +1,68 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout bodyClass="oauth"; section> + <#if section = "header"> + <#if client.attributes.logoUri??> + + +

+ <#if client.name?has_content> + ${msg("oauthGrantTitle",advancedMsg(client.name))} + <#else> + ${msg("oauthGrantTitle",client.clientId)} + +

+ <#elseif section = "form"> +
+

${msg("oauthGrantRequest")}

+
    + <#if oauth.clientScopesRequested??> + <#list oauth.clientScopesRequested as clientScope> +
  • + <#if !clientScope.dynamicScopeParameter??> + ${advancedMsg(clientScope.consentScreenText)} + <#else> + ${advancedMsg(clientScope.consentScreenText)}: ${clientScope.dynamicScopeParameter} + + +
  • + + +
+ <#if client.attributes.policyUri?? || client.attributes.tosUri??> +

+ <#if client.name?has_content> + ${msg("oauthGrantInformation",advancedMsg(client.name))} + <#else> + ${msg("oauthGrantInformation",client.clientId)} + + <#if client.attributes.tosUri??> + ${msg("oauthGrantReview")} + ${msg("oauthGrantTos")} + + <#if client.attributes.policyUri??> + ${msg("oauthGrantReview")} + ${msg("oauthGrantPolicy")} + +

+ + +
+ +
+
+
+
+
+ +
+
+ + +
+
+
+
+
+
+ + diff --git a/themes/src/main/resources/theme/unixdog/login/login-oauth2-device-verify-user-code.ftl b/themes/src/main/resources/theme/unixdog/login/login-oauth2-device-verify-user-code.ftl new file mode 100644 index 0000000000..dfb625fe8b --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/login-oauth2-device-verify-user-code.ftl @@ -0,0 +1,31 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout; section> + <#if section = "header"> + ${msg("oauth2DeviceVerificationTitle")} + <#elseif section = "form"> +
+
+
+ +
+ +
+ +
+
+ +
+
+
+
+
+ +
+
+ +
+
+
+
+ + \ No newline at end of file diff --git a/themes/src/main/resources/theme/unixdog/login/login-otp.ftl b/themes/src/main/resources/theme/unixdog/login/login-otp.ftl new file mode 100755 index 0000000000..a43778d900 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/login-otp.ftl @@ -0,0 +1,58 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=!messagesPerField.existsError('totp'); section> + <#if section="header"> + ${msg("doLogIn")} + <#elseif section="form"> +
+ <#if otpLogin.userOtpCredentials?size gt 1> +
+
+ <#list otpLogin.userOtpCredentials as otpCredential> + checked="checked"> + + +
+
+ + +
+
+ +
+ +
+ + + <#if messagesPerField.existsError('totp')> + + ${kcSanitize(messagesPerField.get('totp'))?no_esc} + + +
+
+ +
+
+
+
+
+ +
+ +
+
+
+ + \ No newline at end of file diff --git a/themes/src/main/resources/theme/unixdog/login/login-page-expired.ftl b/themes/src/main/resources/theme/unixdog/login/login-page-expired.ftl new file mode 100644 index 0000000000..2b470e018b --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/login-page-expired.ftl @@ -0,0 +1,11 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout; section> + <#if section = "header"> + ${msg("pageExpiredTitle")} + <#elseif section = "form"> +

+ ${msg("pageExpiredMsg1")} ${msg("doClickHere")} .
+ ${msg("pageExpiredMsg2")} ${msg("doClickHere")} . +

+ + diff --git a/themes/src/main/resources/theme/unixdog/login/login-password.ftl b/themes/src/main/resources/theme/unixdog/login/login-password.ftl new file mode 100755 index 0000000000..f49e5a4f46 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/login-password.ftl @@ -0,0 +1,32 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=!messagesPerField.existsError('password'); section> + <#if section = "header"> + ${msg("doLogIn")} + <#elseif section = "form"> +
+
+
+
+ + + <#if messagesPerField.existsError('password')> + + ${kcSanitize(messagesPerField.get('password'))?no_esc} + + +
+ +
+ +
+
+
+
+ + + diff --git a/themes/src/main/resources/theme/unixdog/login/login-recovery-authn-code-config.ftl b/themes/src/main/resources/theme/unixdog/login/login-recovery-authn-code-config.ftl new file mode 100644 index 0000000000..5bd3559d73 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/login-recovery-authn-code-config.ftl @@ -0,0 +1,184 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout; section> + +<#if section = "header"> + ${msg("recovery-code-config-header")} +<#elseif section = "form"> + +
+
+ +
+

+ Warning alert: + ${msg("recovery-code-config-warning-title")} +

+
+

${msg("recovery-code-config-warning-message")}

+
+
+ +
    + <#list recoveryAuthnCodesConfigBean.generatedRecoveryAuthnCodesList as code> +
  1. ${code?counter}: ${code[0..3]}-${code[4..7]}-${code[8..]}
  2. + +
+ + +
+ + + +
+ + +
+ + +
+ +
+ + + + + <#if isAppInitiatedAction??> + + + <#else> + + +
+ + + + diff --git a/themes/src/main/resources/theme/unixdog/login/login-recovery-authn-code-input.ftl b/themes/src/main/resources/theme/unixdog/login/login-recovery-authn-code-input.ftl new file mode 100644 index 0000000000..8d67d434c8 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/login-recovery-authn-code-input.ftl @@ -0,0 +1,44 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=!messagesPerField.existsError('recoveryCodeInput'); section> + + <#if section = "header"> + ${msg("auth-recovery-code-header")} + <#elseif section = "form"> +
+
+
+ +
+ +
+ + + <#if messagesPerField.existsError('recoveryCodeInput')> + + ${kcSanitize(messagesPerField.get('recoveryCodeInput'))?no_esc} + + +
+
+ +
+
+
+
+
+ +
+ +
+
+
+ + \ No newline at end of file diff --git a/themes/src/main/resources/theme/unixdog/login/login-reset-password.ftl b/themes/src/main/resources/theme/unixdog/login/login-reset-password.ftl new file mode 100755 index 0000000000..800faea1f0 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/login-reset-password.ftl @@ -0,0 +1,39 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayInfo=true displayMessage=!messagesPerField.existsError('username'); section> + <#if section = "header"> + ${msg("emailForgotTitle")} + <#elseif section = "form"> +
+
+
+ +
+
+ + <#if messagesPerField.existsError('username')> + + ${kcSanitize(messagesPerField.get('username'))?no_esc} + + +
+
+ +
+ <#elseif section = "info" > + <#if realm.duplicateEmailsAllowed> + ${msg("emailInstructionUsername")} + <#else> + ${msg("emailInstruction")} + + + diff --git a/themes/src/main/resources/theme/unixdog/login/login-update-password.ftl b/themes/src/main/resources/theme/unixdog/login/login-update-password.ftl new file mode 100755 index 0000000000..b884d75271 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/login-update-password.ftl @@ -0,0 +1,71 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=!messagesPerField.existsError('password','password-confirm'); section> + <#if section = "header"> + ${msg("updatePasswordTitle")} + <#elseif section = "form"> +
+ + + +
+
+ +
+
+ + + <#if messagesPerField.existsError('password')> + + ${kcSanitize(messagesPerField.get('password'))?no_esc} + + +
+
+ +
+
+ +
+
+ + + <#if messagesPerField.existsError('password-confirm')> + + ${kcSanitize(messagesPerField.get('password-confirm'))?no_esc} + + + +
+
+ +
+
+
+ <#if isAppInitiatedAction??> +
+ +
+ +
+
+ +
+ <#if isAppInitiatedAction??> + + + <#else> + + +
+
+
+ + \ No newline at end of file diff --git a/themes/src/main/resources/theme/unixdog/login/login-update-profile.ftl b/themes/src/main/resources/theme/unixdog/login/login-update-profile.ftl new file mode 100755 index 0000000000..be579b01bd --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/login-update-profile.ftl @@ -0,0 +1,99 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username','email','firstName','lastName'); section> + <#if section = "header"> + ${msg("loginProfileTitle")} + <#elseif section = "form"> +
+ <#if user.editUsernameAllowed> +
+
+ +
+
+ + + <#if messagesPerField.existsError('username')> + + ${kcSanitize(messagesPerField.get('username'))?no_esc} + + +
+
+ + <#if user.editEmailAllowed> +
+
+ +
+
+ + + <#if messagesPerField.existsError('email')> + + ${kcSanitize(messagesPerField.get('email'))?no_esc} + + +
+
+ + +
+
+ +
+
+ + + <#if messagesPerField.existsError('firstName')> + + ${kcSanitize(messagesPerField.get('firstName'))?no_esc} + + +
+
+ +
+
+ +
+
+ + + <#if messagesPerField.existsError('lastName')> + + ${kcSanitize(messagesPerField.get('lastName'))?no_esc} + + +
+
+ +
+
+
+
+
+ +
+ <#if isAppInitiatedAction??> + + + <#else> + + +
+
+
+ + diff --git a/themes/src/main/resources/theme/unixdog/login/login-username.ftl b/themes/src/main/resources/theme/unixdog/login/login-username.ftl new file mode 100755 index 0000000000..0c18cb7ccb --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/login-username.ftl @@ -0,0 +1,89 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username') displayInfo=(realm.password && realm.registrationAllowed && !registrationDisabled??); section> + <#if section = "header"> + ${msg("loginAccountTitle")} + <#elseif section = "form"> +
+
+ <#if realm.password> +
+ <#if !usernameHidden??> +
+ + + + + <#if messagesPerField.existsError('username')> + + ${kcSanitize(messagesPerField.get('username'))?no_esc} + + +
+ + +
+
+ <#if realm.rememberMe && !usernameHidden??> +
+ +
+ +
+
+ +
+ +
+
+ +
+
+ + <#elseif section = "info" > + <#if realm.password && realm.registrationAllowed && !registrationDisabled??> +
+ ${msg("noAccount")} ${msg("doRegister")} +
+ + <#elseif section = "socialProviders" > + <#if realm.password && social.providers??> +
+
+

${msg("identity-provider-login-label")}

+ + +
+ + + + diff --git a/themes/src/main/resources/theme/unixdog/login/login-verify-email.ftl b/themes/src/main/resources/theme/unixdog/login/login-verify-email.ftl new file mode 100755 index 0000000000..b47d8ca8d5 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/login-verify-email.ftl @@ -0,0 +1,14 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayInfo=true; section> + <#if section = "header"> + ${msg("emailVerifyTitle")} + <#elseif section = "form"> +

${msg("emailVerifyInstruction1",user.email)}

+ <#elseif section = "info"> +

+ ${msg("emailVerifyInstruction2")} +
+ ${msg("doClickHere")} ${msg("emailVerifyInstruction3")} +

+ + diff --git a/themes/src/main/resources/theme/unixdog/login/login-x509-info.ftl b/themes/src/main/resources/theme/unixdog/login/login-x509-info.ftl new file mode 100644 index 0000000000..0228b06338 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/login-x509-info.ftl @@ -0,0 +1,55 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout; section> + <#if section = "header"> + ${msg("doLogIn")} + <#elseif section = "form"> + +
+
+ +
+ +
+ <#if x509.formData.subjectDN??> +
+ +
+ <#else> +
+ +
+ +
+ +
+ + <#if x509.formData.isUserEnabled??> +
+ +
+
+ +
+ + +
+ +
+
+
+
+
+ +
+
+ + <#if x509.formData.isUserEnabled??> + + +
+
+
+
+ + + diff --git a/themes/src/main/resources/theme/unixdog/login/login.ftl b/themes/src/main/resources/theme/unixdog/login/login.ftl new file mode 100755 index 0000000000..21f3f9561c --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/login.ftl @@ -0,0 +1,105 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username','password') displayInfo=realm.password && realm.registrationAllowed && !registrationDisabled??; section> + <#if section = "header"> + ${msg("loginAccountTitle")} + <#elseif section = "form"> +
+
+ <#if realm.password> +
+ <#if !usernameHidden??> +
+ + + + + <#if messagesPerField.existsError('username','password')> + + ${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc} + + + +
+ + +
+ + + + + <#if usernameHidden?? && messagesPerField.existsError('username','password')> + + ${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc} + + + +
+ +
+
+ <#if realm.rememberMe && !usernameHidden??> +
+ +
+ +
+
+ <#if realm.resetPasswordAllowed> + ${msg("doForgotPassword")} + +
+ +
+ +
+ value="${auth.selectedCredential}"/> + +
+
+ +
+ +
+ <#elseif section = "info" > + <#if realm.password && realm.registrationAllowed && !registrationDisabled??> +
+
+ ${msg("noAccount")} ${msg("doRegister")} +
+
+ + <#elseif section = "socialProviders" > + <#if realm.password && social.providers??> +
+
+

${msg("identity-provider-login-label")}

+ + +
+ + + + diff --git a/themes/src/main/resources/theme/unixdog/login/logout-confirm.ftl b/themes/src/main/resources/theme/unixdog/login/logout-confirm.ftl new file mode 100644 index 0000000000..6c0b4e97b6 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/logout-confirm.ftl @@ -0,0 +1,38 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout; section> + <#if section = "header"> + ${msg("logoutConfirmTitle")} + <#elseif section = "form"> +
+

${msg("logoutConfirmHeader")}

+ +
+ +
+
+
+
+
+ +
+ +
+ +
+
+ +
+ <#if logoutConfirm.skipLink> + <#else> + <#if (client.baseUrl)?has_content> +

${kcSanitize(msg("backToApplication"))?no_esc}

+ + +
+ +
+
+ + diff --git a/themes/src/main/resources/theme/unixdog/login/messages/messages_en.properties b/themes/src/main/resources/theme/unixdog/login/messages/messages_en.properties new file mode 100755 index 0000000000..9aed132290 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/messages/messages_en.properties @@ -0,0 +1,511 @@ +doLogIn=Sign In +doRegister=Register +doCancel=Cancel +doSubmit=Submit +doBack=Back +doYes=Yes +doNo=No +doContinue=Continue +doIgnore=Ignore +doAccept=Accept +doDecline=Decline +doForgotPassword=Forgot Password? +doClickHere=Click here +doImpersonate=Impersonate +doTryAgain=Try again +doTryAnotherWay=Try Another Way +doConfirmDelete=Confirm deletion +errorDeletingAccount=Error happened while deleting account +deletingAccountForbidden=You do not have enough permissions to delete your own account, contact admin. +kerberosNotConfigured=Kerberos Not Configured +kerberosNotConfiguredTitle=Kerberos Not Configured +bypassKerberosDetail=Either you are not logged in by Kerberos or your browser is not set up for Kerberos login. Please click continue to login in through other means +kerberosNotSetUp=Kerberos is not set up. You cannot login. +registerTitle=Register +loginAccountTitle=Sign in to your account +loginTitle=Sign in to {0} +loginTitleHtml={0} +impersonateTitle={0} Impersonate User +impersonateTitleHtml={0} Impersonate User +realmChoice=Realm +unknownUser=Unknown user +loginTotpTitle=Mobile Authenticator Setup +loginProfileTitle=Update Account Information +loginIdpReviewProfileTitle=Update Account Information +loginTimeout=Your login attempt timed out. Login will start from the beginning. +reauthenticate=Please re-authenticate to continue +oauthGrantTitle=Grant Access to {0} +oauthGrantTitleHtml={0} +oauthGrantInformation=Make sure you trust {0} by learning how {0} will handle your data. +oauthGrantReview=You could review the +oauthGrantTos=terms of service. +oauthGrantPolicy=privacy policy. +errorTitle=We are sorry... +errorTitleHtml=We are sorry ... +emailVerifyTitle=Email verification +emailForgotTitle=Forgot Your Password? +updateEmailTitle=Update email +emailUpdateConfirmationSentTitle=Confirmation email sent +emailUpdateConfirmationSent=A confirmation email has been sent to {0}. You must follow the instructions of the former to complete the email update. +emailUpdatedTitle=Email updated +emailUpdated=The account email has been successfully updated to {0}. +updatePasswordTitle=Update password +codeSuccessTitle=Success code +codeErrorTitle=Error code\: {0} +displayUnsupported=Requested display type unsupported +browserRequired=Browser required to login +browserContinue=Browser required to complete login +browserContinuePrompt=Open browser and continue login? [y/n]: +browserContinueAnswer=y + +# Transports +usb=USB +nfc=NFC +bluetooth=Bluetooth +internal=Internal +unknown=Unknown + +termsTitle=Terms and Conditions +termsText=

Terms and conditions to be defined

+termsPlainText=Terms and conditions to be defined. + +recaptchaFailed=Invalid Recaptcha +recaptchaNotConfigured=Recaptcha is required, but not configured +consentDenied=Consent denied. + +noAccount=New user? +username=Username +usernameOrEmail=Username or email +firstName=First name +givenName=Given name +fullName=Full name +lastName=Last name +familyName=Family name +email=Email +password=Password +passwordConfirm=Confirm password +passwordNew=New Password +passwordNewConfirm=New Password confirmation +rememberMe=Remember me +authenticatorCode=One-time code +address=Address +street=Street +locality=City or Locality +region=State, Province, or Region +postal_code=Zip or Postal code +country=Country +emailVerified=Email verified +website=Web page +phoneNumber=Phone number +phoneNumberVerified=Phone number verified +gender=Gender +birthday=Birthdate +zoneinfo=Time zone +gssDelegationCredential=GSS Delegation Credential +logoutOtherSessions=Sign out from other devices + +profileScopeConsentText=User profile +emailScopeConsentText=Email address +addressScopeConsentText=Address +phoneScopeConsentText=Phone number +offlineAccessScopeConsentText=Offline Access +samlRoleListScopeConsentText=My Roles +rolesScopeConsentText=User roles + +restartLoginTooltip=Restart login + +loginTotpIntro=You need to set up a One Time Password generator to access this account +loginTotpStep1=Install one of the following applications on your mobile: +loginTotpStep2=Open the application and scan the barcode: +loginTotpStep3=Enter the one-time code provided by the application and click Submit to finish the setup. +loginTotpStep3DeviceName=Provide a Device Name to help you manage your OTP devices. +loginTotpManualStep2=Open the application and enter the key: +loginTotpManualStep3=Use the following configuration values if the application allows setting them: +loginTotpUnableToScan=Unable to scan? +loginTotpScanBarcode=Scan barcode? +loginCredential=Credential +loginOtpOneTime=One-time code +loginTotpType=Type +loginTotpAlgorithm=Algorithm +loginTotpDigits=Digits +loginTotpInterval=Interval +loginTotpCounter=Counter +loginTotpDeviceName=Device Name + +loginTotp.totp=Time-based +loginTotp.hotp=Counter-based + +totpAppFreeOTPName=FreeOTP +totpAppGoogleName=Google Authenticator +totpAppMicrosoftAuthenticatorName=Microsoft Authenticator + +loginChooseAuthenticator=Select login method + +oauthGrantRequest=Do you grant these access privileges? +inResource=in + +oauth2DeviceVerificationTitle=Device Login +verifyOAuth2DeviceUserCode=Enter the code provided by your device and click Submit +oauth2DeviceInvalidUserCodeMessage=Invalid code, please try again. +oauth2DeviceExpiredUserCodeMessage=The code has expired. Please go back to your device and try connecting again. +oauth2DeviceVerificationCompleteHeader=Device Login Successful +oauth2DeviceVerificationCompleteMessage=You may close this browser window and go back to your device. +oauth2DeviceVerificationFailedHeader=Device Login Failed +oauth2DeviceVerificationFailedMessage=You may close this browser window and go back to your device and try connecting again. +oauth2DeviceConsentDeniedMessage=Consent denied for connecting the device. +oauth2DeviceAuthorizationGrantDisabledMessage=Client is not allowed to initiate OAuth 2.0 Device Authorization Grant. The flow is disabled for the client. + +emailVerifyInstruction1=An email with instructions to verify your email address has been sent to your address {0}. +emailVerifyInstruction2=Haven''t received a verification code in your email? +emailVerifyInstruction3=to re-send the email. + +emailLinkIdpTitle=Link {0} +emailLinkIdp1=An email with instructions to link {0} account {1} with your {2} account has been sent to you. +emailLinkIdp2=Haven''t received a verification code in your email? +emailLinkIdp3=to re-send the email. +emailLinkIdp4=If you already verified the email in different browser +emailLinkIdp5=to continue. + +backToLogin=« Back to Login + +emailInstruction=Enter your username or email address and we will send you instructions on how to create a new password. +emailInstructionUsername=Enter your username and we will send you instructions on how to create a new password. + +copyCodeInstruction=Please copy this code and paste it into your application: + +pageExpiredTitle=Page has expired +pageExpiredMsg1=To restart the login process +pageExpiredMsg2=To continue the login process + +personalInfo=Personal Info: +role_admin=Admin +role_realm-admin=Realm Admin +role_create-realm=Create realm +role_create-client=Create client +role_view-realm=View realm +role_view-users=View users +role_view-applications=View applications +role_view-clients=View clients +role_view-events=View events +role_view-identity-providers=View identity providers +role_manage-realm=Manage realm +role_manage-users=Manage users +role_manage-applications=Manage applications +role_manage-identity-providers=Manage identity providers +role_manage-clients=Manage clients +role_manage-events=Manage events +role_view-profile=View profile +role_manage-account=Manage account +role_manage-account-links=Manage account links +role_read-token=Read token +role_offline-access=Offline access +client_account=Account +client_account-console=Account Console +client_security-admin-console=Security Admin Console +client_admin-cli=Admin CLI +client_realm-management=Realm Management +client_broker=Broker + +requiredFields=Required fields + +invalidUserMessage=Invalid username or password. +invalidUsernameMessage=Invalid username. +invalidUsernameOrEmailMessage=Invalid username or email. +invalidPasswordMessage=Invalid password. +invalidEmailMessage=Invalid email address. +accountDisabledMessage=Account is disabled, contact your administrator. +accountTemporarilyDisabledMessage=Account is temporarily disabled; contact your administrator or retry later. +expiredCodeMessage=Login timeout. Please sign in again. +expiredActionMessage=Action expired. Please continue with login now. +expiredActionTokenNoSessionMessage=Action expired. +expiredActionTokenSessionExistsMessage=Action expired. Please start again. +sessionLimitExceeded=There are too many sessions + +missingFirstNameMessage=Please specify first name. +missingLastNameMessage=Please specify last name. +missingEmailMessage=Please specify email. +missingUsernameMessage=Please specify username. +missingPasswordMessage=Please specify password. +missingTotpMessage=Please specify authenticator code. +missingTotpDeviceNameMessage=Please specify device name. +notMatchPasswordMessage=Passwords don''t match. + +error-invalid-value=Invalid value. +error-invalid-blank=Please specify value. +error-empty=Please specify value. +error-invalid-length=Length must be between {1} and {2}. +error-invalid-length-too-short=Minimal length is {1}. +error-invalid-length-too-long=Maximal length is {2}. +error-invalid-email=Invalid email address. +error-invalid-number=Invalid number. +error-number-out-of-range=Number must be between {1} and {2}. +error-number-out-of-range-too-small=Number must have minimal value of {1}. +error-number-out-of-range-too-big=Number must have maximal value of {2}. +error-pattern-no-match=Invalid value. +error-invalid-uri=Invalid URL. +error-invalid-uri-scheme=Invalid URL scheme. +error-invalid-uri-fragment=Invalid URL fragment. +error-user-attribute-required=Please specify this field. +error-invalid-date=Invalid date. +error-user-attribute-read-only=This field is read only. +error-username-invalid-character=Value contains invalid character. +error-person-name-invalid-character=Value contains invalid character. + +invalidPasswordExistingMessage=Invalid existing password. +invalidPasswordBlacklistedMessage=Invalid password: password is blacklisted. +invalidPasswordConfirmMessage=Password confirmation doesn''t match. +invalidTotpMessage=Invalid authenticator code. + +usernameExistsMessage=Username already exists. +emailExistsMessage=Email already exists. + +federatedIdentityExistsMessage=User with {0} {1} already exists. Please login to account management to link the account. +federatedIdentityUnavailableMessage=User {0} authenticated with identity provider {1} does not exist. Please contact your administrator. + +confirmLinkIdpTitle=Account already exists +federatedIdentityConfirmLinkMessage=User with {0} {1} already exists. How do you want to continue? +federatedIdentityConfirmReauthenticateMessage=Authenticate to link your account with {0} +nestedFirstBrokerFlowMessage=The {0} user {1} is not linked to any known user. +confirmLinkIdpReviewProfile=Review profile +confirmLinkIdpContinue=Add to existing account + +configureTotpMessage=You need to set up Mobile Authenticator to activate your account. +configureBackupCodesMessage=You need to set up Backup Codes to activate your account. +updateProfileMessage=You need to update your user profile to activate your account. +updatePasswordMessage=You need to change your password to activate your account. +updateEmailMessage=You need to update your email address to activate your account. +resetPasswordMessage=You need to change your password. +verifyEmailMessage=You need to verify your email address to activate your account. +linkIdpMessage=You need to verify your email address to link your account with {0}. + +emailSentMessage=You should receive an email shortly with further instructions. +emailSendErrorMessage=Failed to send email, please try again later. + +accountUpdatedMessage=Your account has been updated. +accountPasswordUpdatedMessage=Your password has been updated. + +delegationCompleteHeader=Login Successful +delegationCompleteMessage=You may close this browser window and go back to your console application. +delegationFailedHeader=Login Failed +delegationFailedMessage=You may close this browser window and go back to your console application and try logging in again. + +noAccessMessage=No access + +invalidPasswordMinLengthMessage=Invalid password: minimum length {0}. +invalidPasswordMaxLengthMessage=Invalid password: maximum length {0}. +invalidPasswordMinDigitsMessage=Invalid password: must contain at least {0} numerical digits. +invalidPasswordMinLowerCaseCharsMessage=Invalid password: must contain at least {0} lower case characters. +invalidPasswordMinUpperCaseCharsMessage=Invalid password: must contain at least {0} upper case characters. +invalidPasswordMinSpecialCharsMessage=Invalid password: must contain at least {0} special characters. +invalidPasswordNotUsernameMessage=Invalid password: must not be equal to the username. +invalidPasswordNotEmailMessage=Invalid password: must not be equal to the email. +invalidPasswordRegexPatternMessage=Invalid password: fails to match regex pattern(s). +invalidPasswordHistoryMessage=Invalid password: must not be equal to any of last {0} passwords. +invalidPasswordGenericMessage=Invalid password: new password doesn''t match password policies. + +failedToProcessResponseMessage=Failed to process response +httpsRequiredMessage=HTTPS required +realmNotEnabledMessage=Realm not enabled +invalidRequestMessage=Invalid Request +successLogout=You are logged out +failedLogout=Logout failed +unknownLoginRequesterMessage=Unknown login requester +loginRequesterNotEnabledMessage=Login requester not enabled +bearerOnlyMessage=Bearer-only applications are not allowed to initiate browser login +standardFlowDisabledMessage=Client is not allowed to initiate browser login with given response_type. Standard flow is disabled for the client. +implicitFlowDisabledMessage=Client is not allowed to initiate browser login with given response_type. Implicit flow is disabled for the client. +invalidRedirectUriMessage=Invalid redirect uri +unsupportedNameIdFormatMessage=Unsupported NameIDFormat +invalidRequesterMessage=Invalid requester +registrationNotAllowedMessage=Registration not allowed +resetCredentialNotAllowedMessage=Reset Credential not allowed + +permissionNotApprovedMessage=Permission not approved. +noRelayStateInResponseMessage=No relay state in response from identity provider. +insufficientPermissionMessage=Insufficient permissions to link identities. +couldNotProceedWithAuthenticationRequestMessage=Could not proceed with authentication request to identity provider. +couldNotObtainTokenMessage=Could not obtain token from identity provider. +unexpectedErrorRetrievingTokenMessage=Unexpected error when retrieving token from identity provider. +unexpectedErrorHandlingResponseMessage=Unexpected error when handling response from identity provider. +identityProviderAuthenticationFailedMessage=Authentication failed. Could not authenticate with identity provider. +couldNotSendAuthenticationRequestMessage=Could not send authentication request to identity provider. +unexpectedErrorHandlingRequestMessage=Unexpected error when handling authentication request to identity provider. +invalidAccessCodeMessage=Invalid access code. +sessionNotActiveMessage=Session not active. +invalidCodeMessage=An error occurred, please login again through your application. +cookieNotFoundMessage=Cookie not found. Please make sure cookies are enabled in your browser. +insufficientLevelOfAuthentication=The requested level of authentication has not been satisfied. +identityProviderUnexpectedErrorMessage=Unexpected error when authenticating with identity provider +identityProviderMissingStateMessage=Missing state parameter in response from identity provider. +identityProviderInvalidResponseMessage=Invalid response from identity provider. +identityProviderInvalidSignatureMessage=Invalid signature in response from identity provider. +identityProviderNotFoundMessage=Could not find an identity provider with the identifier. +identityProviderLinkSuccess=You successfully verified your email. Please go back to your original browser and continue there with the login. +staleCodeMessage=This page is no longer valid, please go back to your application and sign in again +realmSupportsNoCredentialsMessage=Realm does not support any credential type. +credentialSetupRequired=Cannot login, credential setup required. +identityProviderNotUniqueMessage=Realm supports multiple identity providers. Could not determine which identity provider should be used to authenticate with. +emailVerifiedMessage=Your email address has been verified. +staleEmailVerificationLink=The link you clicked is an old stale link and is no longer valid. Maybe you have already verified your email. +identityProviderAlreadyLinkedMessage=Federated identity returned by {0} is already linked to another user. +confirmAccountLinking=Confirm linking the account {0} of identity provider {1} with your account. +confirmEmailAddressVerification=Confirm validity of e-mail address {0}. +confirmExecutionOfActions=Perform the following action(s) + +locale_ar=\u0639\u0631\u0628\u064A +locale_ca=Catal\u00E0 +locale_cs=\u010Ce\u0161tina +locale_da=Dansk +locale_de=Deutsch +locale_en=English +locale_es=Espa\u00F1ol +locale_fr=Fran\u00E7ais +locale_hu=Magyar +locale_it=Italiano +locale_ja=\u65E5\u672C\u8A9E +locale_lt=Lietuvi\u0173 +locale_nl=Nederlands +locale_no=Norsk +locale_pl=Polski +locale_pt_BR=Portugu\u00EAs (Brasil) +locale_pt-BR=Portugu\u00EAs (Brasil) +locale_ru=\u0420\u0443\u0441\u0441\u043A\u0438\u0439 +locale_sk=Sloven\u010Dina +locale_sv=Svenska +locale_tr=T\u00FCrk\u00E7e +locale_zh-CN=\u4E2D\u6587\u7B80\u4F53 +locale_fi=Suomi + +backToApplication=« Back to Application +missingParameterMessage=Missing parameters\: {0} +clientNotFoundMessage=Client not found. +clientDisabledMessage=Client disabled. +invalidParameterMessage=Invalid parameter\: {0} +alreadyLoggedIn=You are already logged in. +differentUserAuthenticated=You are already authenticated as different user ''{0}'' in this session. Please sign out first. +brokerLinkingSessionExpired=Requested broker account linking, but current session is no longer valid. +proceedWithAction=» Click here to proceed +acrNotFulfilled=Authentication requirements not fulfilled + +requiredAction.CONFIGURE_TOTP=Configure OTP +requiredAction.TERMS_AND_CONDITIONS=Terms and Conditions +requiredAction.UPDATE_PASSWORD=Update Password +requiredAction.UPDATE_PROFILE=Update Profile +requiredAction.VERIFY_EMAIL=Verify Email +requiredAction.CONFIGURE_RECOVERY_AUTHN_CODES=Generate Recovery Codes +requiredAction.webauthn-register-passwordless=Webauthn Register Passwordless + +invalidTokenRequiredActions=Required actions included in the link are not valid + +doX509Login=You will be logged in as\: +clientCertificate=X509 client certificate\: +noCertificate=[No Certificate] + + +pageNotFound=Page not found +internalServerError=An internal server error has occurred + +console-username=Username: +console-password=Password: +console-otp=One Time Password: +console-new-password=New Password: +console-confirm-password=Confirm Password: +console-update-password=Update of your password is required. +console-verify-email=You need to verify your email address. We sent an email to {0} that contains a verification code. Please enter this code into the input below. +console-email-code=Email Code: +console-accept-terms=Accept Terms? [y/n]: +console-accept=y + +# Openshift messages +openshift.scope.user_info=User information +openshift.scope.user_check-access=User access information +openshift.scope.user_full=Full Access +openshift.scope.list-projects=List projects + +# SAML authentication +saml.post-form.title=Authentication Redirect +saml.post-form.message=Redirecting, please wait. +saml.post-form.js-disabled=JavaScript is disabled. We strongly recommend to enable it. Click the button below to continue. +saml.artifactResolutionServiceInvalidResponse=Unable to resolve artifact. + +#authenticators +otp-display-name=Authenticator Application +otp-help-text=Enter a verification code from authenticator application. +password-display-name=Password +password-help-text=Sign in by entering your password. +auth-username-form-display-name=Username +auth-username-form-help-text=Start sign in by entering your username +auth-username-password-form-display-name=Username and password +auth-username-password-form-help-text=Sign in by entering your username and password. + +# Recovery Codes +auth-recovery-authn-code-form-display-name=Recovery Authentication Code +auth-recovery-authn-code-form-help-text=Enter a recovery authentication code from a previously generated list. +auth-recovery-code-info-message=Enter the specified recovery code. +auth-recovery-code-prompt=Recovery code #{0} +auth-recovery-code-header=Login with a recovery authentication code +recovery-codes-error-invalid=Invalid recovery authentication code +recovery-code-config-header=Recovery Authentication Codes +recovery-code-config-warning-title=These recovery codes won't appear again after leaving this page +recovery-code-config-warning-message=Make sure to print, download, or copy them to a password manager and keep them save. Canceling this setup will remove these recovery codes from your account. +recovery-codes-print=Print +recovery-codes-download=Download +recovery-codes-copy=Copy +recovery-codes-copied=Copied +recovery-codes-confirmation-message=I have saved these codes somewhere safe +recovery-codes-action-complete=Complete setup +recovery-codes-action-cancel=Cancel setup +recovery-codes-download-file-header=Keep these recovery codes somewhere safe. +recovery-codes-download-file-description=Recovery codes are single-use passcodes that allow you to sign in to your account if you do not have access to your authenticator. +recovery-codes-download-file-date= These codes were generated on +recovery-codes-label-default=Recovery codes + +# WebAuthn +webauthn-display-name=Security Key +webauthn-help-text=Use your security key to sign in. +webauthn-passwordless-display-name=Security Key +webauthn-passwordless-help-text=Use your security key for passwordless sign in. +webauthn-login-title=Security Key login +webauthn-registration-title=Security Key Registration +webauthn-available-authenticators=Available Security Keys +webauthn-unsupported-browser-text=WebAuthn is not supported by this browser. Try another one or contact your administrator. +webauthn-doAuthenticate=Sign in with Security Key +webauthn-createdAt-label=Created + +# WebAuthn Error +webauthn-error-title=Security Key Error +webauthn-error-registration=Failed to register your Security key.
{0} +webauthn-error-api-get=Failed to authenticate by the Security key.
{0} +webauthn-error-different-user=First authenticated user is not the one authenticated by the Security key. +webauthn-error-auth-verification=Security key authentication result is invalid.
{0} +webauthn-error-register-verification=Security key registration result is invalid.
{0} +webauthn-error-user-not-found=Unknown user authenticated by the Security key. + +# Identity provider +identity-provider-redirector=Connect with another Identity Provider +identity-provider-login-label=Or sign in with +idp-email-verification-display-name=Email Verification +idp-email-verification-help-text=Link your account by validating your email. +idp-username-password-form-display-name=Username and password +idp-username-password-form-help-text=Link your account by logging in. + +finalDeletionConfirmation=If you delete your account, it cannot be restored. To keep your account, click Cancel. +irreversibleAction=This action is irreversible +deleteAccountConfirm=Delete account confirmation + +deletingImplies=Deleting your account implies: +errasingData=Erasing all your data +loggingOutImmediately=Logging you out immediately +accountUnusable=Any subsequent use of the application will not be possible with this account +userDeletedSuccessfully=User deleted successfully + +access-denied=Access denied +access-denied-when-idp-auth=Access denied when authenticating with {0} + +frontchannel-logout.title=Logging out +frontchannel-logout.message=You are logging out from following apps +logoutConfirmTitle=Logging out +logoutConfirmHeader=Do you want to logout? +doLogout=Logout + +readOnlyUsernameMessage=You can''t update your username as it is read-only. diff --git a/themes/src/main/resources/theme/unixdog/login/register-user-profile.ftl b/themes/src/main/resources/theme/unixdog/login/register-user-profile.ftl new file mode 100755 index 0000000000..e0d533b89f --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/register-user-profile.ftl @@ -0,0 +1,74 @@ +<#import "template.ftl" as layout> +<#import "user-profile-commons.ftl" as userProfileCommons> +<@layout.registrationLayout displayMessage=messagesPerField.exists('global') displayRequiredFields=true; section> + <#if section = "header"> + ${msg("registerTitle")} + <#elseif section = "form"> +
+ + <@userProfileCommons.userProfileFormFields; callback, attribute> + <#if callback = "afterField"> + <#-- render password fields just under the username or email (if used as username) --> + <#if passwordRequired?? && (attribute.name == 'username' || (attribute.name == 'email' && realm.registrationEmailAsUsername))> +
+
+ * +
+
+ + + <#if messagesPerField.existsError('password')> + + ${kcSanitize(messagesPerField.get('password'))?no_esc} + + +
+
+ +
+
+ * +
+
+ + + <#if messagesPerField.existsError('password-confirm')> + + ${kcSanitize(messagesPerField.get('password-confirm'))?no_esc} + + +
+
+ + + + + <#if recaptchaRequired??> +
+
+
+
+
+ + + +
+ + \ No newline at end of file diff --git a/themes/src/main/resources/theme/unixdog/login/register.ftl b/themes/src/main/resources/theme/unixdog/login/register.ftl new file mode 100755 index 0000000000..db50984798 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/register.ftl @@ -0,0 +1,141 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=!messagesPerField.existsError('firstName','lastName','email','username','password','password-confirm'); section> + <#if section = "header"> + ${msg("registerTitle")} + <#elseif section = "form"> +
+
+
+ +
+
+ + + <#if messagesPerField.existsError('firstName')> + + ${kcSanitize(messagesPerField.get('firstName'))?no_esc} + + +
+
+ +
+
+ +
+
+ + + <#if messagesPerField.existsError('lastName')> + + ${kcSanitize(messagesPerField.get('lastName'))?no_esc} + + +
+
+ +
+
+ +
+
+ + + <#if messagesPerField.existsError('email')> + + ${kcSanitize(messagesPerField.get('email'))?no_esc} + + +
+
+ + <#if !realm.registrationEmailAsUsername> +
+
+ +
+
+ + + <#if messagesPerField.existsError('username')> + + ${kcSanitize(messagesPerField.get('username'))?no_esc} + + +
+
+ + + <#if passwordRequired??> +
+
+ +
+
+ + + <#if messagesPerField.existsError('password')> + + ${kcSanitize(messagesPerField.get('password'))?no_esc} + + +
+
+ +
+
+ +
+
+ + + <#if messagesPerField.existsError('password-confirm')> + + ${kcSanitize(messagesPerField.get('password-confirm'))?no_esc} + + +
+
+ + + <#if recaptchaRequired??> +
+
+
+
+
+ + + +
+ + \ No newline at end of file diff --git a/themes/src/main/resources/theme/unixdog/login/resources/css/main.css b/themes/src/main/resources/theme/unixdog/login/resources/css/main.css new file mode 100644 index 0000000000..ff2d6d6778 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/resources/css/main.css @@ -0,0 +1,209 @@ +body { + font-family: monospace; + margin: 0; + background: #080e08; + color: #f6f6f6; + + display: flex; + flex-direction: column; + min-height: 100vh; +} + +h1, h2 { + color: #4af626; +} + +footer { + padding: 10px; + max-width: 800px; + width: 100%; + background: #3a7920; + color: #f6f6f6; + + margin: auto auto 0; + box-sizing: border-box; +} + +a:link { + color: #2cacb0; +} + +a:visited { + color: #b4778f; +} +@media (prefers-color-scheme: light) { + body { + background: #fcfffc; + color: #211c1b; + } + + h1, h2 { + color: #4c982a; + } + + a:link { + color: #1f7b7e; + } + + a:visited { + color: #7c5263; + } + + textarea { + background: #ffffff; + color: #211c1b; + } +} + +header { + background: linear-gradient( + 90deg, + rgba(255, 0, 0, 1) 0%, + rgba(255, 154, 0, 1) 10%, + rgba(208, 222, 33, 1) 20%, + rgba(79, 220, 74, 1) 30%, + rgba(63, 218, 216, 1) 40%, + rgba(47, 201, 226, 1) 50%, + rgba(28, 127, 238, 1) 60%, + rgba(95, 21, 242, 1) 70%, + rgba(186, 12, 248, 1) 80%, + rgba(251, 7, 217, 1) 90%, + rgba(255, 0, 0, 1) 100% + ); + min-height: 50px; + display: flex; + color: black; + box-sizing: border-box; +} + +#header-content { + max-width: 800px; + margin: auto; + width: 100%; + align-content: center; + flex-direction: row; + display: flex; + padding: 10px; +} +#header-content h1 { + color: black; + margin: auto auto auto 1rem; +} + +#header-content img { + display: inline; +} + +main { + max-width: 800px; + margin: 0 auto; + padding: 10px; + width: 100%; + box-sizing: border-box; +} + + +span.copyleft { + display: inline-block; + transform: rotate(180deg); +} + +footer a:link, footer a:visited { + text-decoration: none; + font-weight: bold; + color: white; +} + +footer a:hover { + text-decoration: underline; +} + + +input { + background: #332c29; + color: #f6f6f6; +} + +textarea { + background: #332c29; + color: #f6f6f6; +} + +textarea.big { + width: 100%; + height: 5em; +} + +.error, .required { + color: #f00; +} + +#nav { + margin: auto 0 auto auto; + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +#nav a { + display: block; + padding: 10px; +} + +pre { + white-space: pre-wrap; +} + +#header-content a:link { + text-decoration: none; + color: black; +} +#header-content a:visited { + text-decoration: none; + color: black; +} + +#header-content a:hover { + text-decoration: underline; +} + +label.field-with-error { + color: red; +} + +img.stats { + width: 100%; + display: block; +} + +/* Tables */ +table { + border-collapse: collapse; + width: 100%; +} +th, td { + padding: 5px; +} +th { + border-bottom: solid 2px currentColor; +} + +td:first-child { + border-right: solid 2px currentColor; + border-left: none; +} +td { + border-right: solid 2px currentColor; + border-left: solid 2px currentColor; +} +td:last-child { + border-right: none; + border-left: solid 2px currentColor; +} + +/* Responsive */ +.responsive-wrapper { + overflow-x: auto; + width: 100%; +} + diff --git a/themes/src/main/resources/theme/unixdog/login/resources/img/favicon.ico b/themes/src/main/resources/theme/unixdog/login/resources/img/favicon.ico new file mode 100644 index 0000000000..46eafce9bc Binary files /dev/null and b/themes/src/main/resources/theme/unixdog/login/resources/img/favicon.ico differ diff --git a/themes/src/main/resources/theme/unixdog/login/resources/js/base64url.js b/themes/src/main/resources/theme/unixdog/login/resources/js/base64url.js new file mode 100644 index 0000000000..64555bfbd5 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/resources/js/base64url.js @@ -0,0 +1,114 @@ +// for embedded scripts, quoted and modified from https://github.com/swansontec/rfc4648.js by William Swanson +'use strict'; +var base64url = base64url || {}; +(function(base64url) { + + function parse (string, encoding, opts = {}) { + // Build the character lookup table: + if (!encoding.codes) { + encoding.codes = {}; + for (let i = 0; i < encoding.chars.length; ++i) { + encoding.codes[encoding.chars[i]] = i; + } + } + + // The string must have a whole number of bytes: + if (!opts.loose && (string.length * encoding.bits) & 7) { + throw new SyntaxError('Invalid padding'); + } + + // Count the padding bytes: + let end = string.length; + while (string[end - 1] === '=') { + --end; + + // If we get a whole number of bytes, there is too much padding: + if (!opts.loose && !(((string.length - end) * encoding.bits) & 7)) { + throw new SyntaxError('Invalid padding'); + } + } + + // Allocate the output: + const out = new (opts.out || Uint8Array)(((end * encoding.bits) / 8) | 0); + + // Parse the data: + let bits = 0; // Number of bits currently in the buffer + let buffer = 0; // Bits waiting to be written out, MSB first + let written = 0; // Next byte to write + for (let i = 0; i < end; ++i) { + // Read one character from the string: + const value = encoding.codes[string[i]]; + if (value === void 0) { + throw new SyntaxError('Invalid character ' + string[i]); + } + + // Append the bits to the buffer: + buffer = (buffer << encoding.bits) | value; + bits += encoding.bits; + + // Write out some bits if the buffer has a byte's worth: + if (bits >= 8) { + bits -= 8; + out[written++] = 0xff & (buffer >> bits); + } + } + + // Verify that we have received just enough bits: + if (bits >= encoding.bits || 0xff & (buffer << (8 - bits))) { + throw new SyntaxError('Unexpected end of data'); + } + + return out + } + + function stringify (data, encoding, opts = {}) { + const { pad = true } = opts; + const mask = (1 << encoding.bits) - 1; + let out = ''; + + let bits = 0; // Number of bits currently in the buffer + let buffer = 0; // Bits waiting to be written out, MSB first + for (let i = 0; i < data.length; ++i) { + // Slurp data into the buffer: + buffer = (buffer << 8) | (0xff & data[i]); + bits += 8; + + // Write out as much as we can: + while (bits > encoding.bits) { + bits -= encoding.bits; + out += encoding.chars[mask & (buffer >> bits)]; + } + } + + // Partial character: + if (bits) { + out += encoding.chars[mask & (buffer << (encoding.bits - bits))]; + } + + // Add padding characters until we hit a byte boundary: + if (pad) { + while ((out.length * encoding.bits) & 7) { + out += '='; + } + } + + return out + } + + const encoding = { + chars: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_', + bits: 6 + } + + base64url.decode = function (string, opts) { + return parse(string, encoding, opts); + } + + base64url.encode = function (data, opts) { + return stringify(data, encoding, opts) + } + + return base64url; +}(base64url)); + + diff --git a/themes/src/main/resources/theme/unixdog/login/saml-post-form.ftl b/themes/src/main/resources/theme/unixdog/login/saml-post-form.ftl new file mode 100644 index 0000000000..94b0c30fca --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/saml-post-form.ftl @@ -0,0 +1,25 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout; section> + <#if section = "header"> + ${msg("saml.post-form.title")} + <#elseif section = "form"> + +

${msg("saml.post-form.message")}

+
+ <#if samlPost.SAMLRequest??> + + + <#if samlPost.SAMLResponse??> + + + <#if samlPost.relayState??> + + + + +
+ + diff --git a/themes/src/main/resources/theme/unixdog/login/select-authenticator.ftl b/themes/src/main/resources/theme/unixdog/login/select-authenticator.ftl new file mode 100644 index 0000000000..769715b832 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/select-authenticator.ftl @@ -0,0 +1,39 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayInfo=false; section> + <#if section = "header" || section = "show-username"> + + <#if section = "header"> + ${msg("loginChooseAuthenticator")} + + <#elseif section = "form"> +
+ + + + + + + <#list auth.authenticationSelections as authenticationSelection> + + + + + + +
MethodDescriptionAction
${msg('${authenticationSelection.displayName}')}${msg('${authenticationSelection.helpText}')} +
+ + +
+
+
+ + + + diff --git a/themes/src/main/resources/theme/unixdog/login/template.ftl b/themes/src/main/resources/theme/unixdog/login/template.ftl new file mode 100644 index 0000000000..b5f6a5159d --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/template.ftl @@ -0,0 +1,162 @@ +<#macro registrationLayout bodyClass="" displayInfo=false displayMessage=true displayRequiredFields=false> + + + + + + + + + <#if properties.meta?has_content> + <#list properties.meta?split(' ') as meta> + + + + ${msg("loginTitle",(realm.displayName!''))} + + <#if properties.stylesCommon?has_content> + <#list properties.stylesCommon?split(' ') as style> + + + + <#if properties.styles?has_content> + <#list properties.styles?split(' ') as style> + + + + <#if properties.scripts?has_content> + <#list properties.scripts?split(' ') as script> + + + + <#if scripts??> + <#list scripts as script> + + + + + + +
+
+ UNIX.dog +

${kcSanitize(msg("loginTitleHtml",(realm.displayNameHtml!'')))?no_esc}

+ +
+
+
+
+
+ + <#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())> + <#if displayRequiredFields> +
+
+ * ${msg("requiredFields")} +
+
+

<#nested "header">

+
+
+ <#else> +

<#nested "header">

+ + <#else> + <#if displayRequiredFields> +
+
+ * ${msg("requiredFields")} +
+
+ <#nested "show-username"> +
+

Logging in as: + . + + ${msg("restartLoginTooltip")}. + +

+
+
+
+ <#else> + <#nested "show-username"> +
+

Logging in as: + . + + ${msg("restartLoginTooltip")}. + +

+
+ + +
+
+ + <#-- App-initiated actions should not see warning messages about the need to complete the action --> + <#-- during login. --> + <#if displayMessage && message?has_content && (message.type != 'warning' || !isAppInitiatedAction??)> +
+ ${kcSanitize(message.summary)?no_esc} +
+ + + <#nested "form"> + + <#if auth?has_content && auth.showTryAnotherWayLink()> +
+
+ + +
+
+ + + <#if displayInfo> +
+
+ <#nested "info"> +
+
+ +
+
+ +
+
+
+
+ 🄯 Copyleft UNIX.dog, + 2023. All pages licensed under CC BY-NC-SA 4.0. +
+Please follow all rules while using these services. +
+ + + diff --git a/themes/src/main/resources/theme/unixdog/login/terms.ftl b/themes/src/main/resources/theme/unixdog/login/terms.ftl new file mode 100755 index 0000000000..687b192c94 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/terms.ftl @@ -0,0 +1,15 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=false; section> + <#if section = "header"> + ${msg("termsTitle")} + <#elseif section = "form"> +
+ ${kcSanitize(msg("termsText"))?no_esc} +
+
+ + +
+
+ + diff --git a/themes/src/main/resources/theme/unixdog/login/theme.properties b/themes/src/main/resources/theme/unixdog/login/theme.properties new file mode 100644 index 0000000000..c6ef88537d --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/theme.properties @@ -0,0 +1,4 @@ + +styles=css/main.css + +kcInputErrorMessageClass=error diff --git a/themes/src/main/resources/theme/unixdog/login/update-email.ftl b/themes/src/main/resources/theme/unixdog/login/update-email.ftl new file mode 100644 index 0000000000..4c85e5b5da --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/update-email.ftl @@ -0,0 +1,42 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=!messagesPerField.existsError('email'); section> + <#if section = "header"> + ${msg("updateEmailTitle")} + <#elseif section = "form"> +
+
+
+ +
+
+ + + <#if messagesPerField.existsError('email')> + + ${kcSanitize(messagesPerField.get('email'))?no_esc} + + +
+
+ +
+
+
+
+
+ +
+ <#if isAppInitiatedAction??> + + + <#else> + + +
+
+
+ + diff --git a/themes/src/main/resources/theme/unixdog/login/update-user-profile.ftl b/themes/src/main/resources/theme/unixdog/login/update-user-profile.ftl new file mode 100755 index 0000000000..e09f5c3884 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/update-user-profile.ftl @@ -0,0 +1,28 @@ +<#import "template.ftl" as layout> +<#import "user-profile-commons.ftl" as userProfileCommons> +<@layout.registrationLayout displayMessage=messagesPerField.exists('global') displayRequiredFields=true; section> + <#if section = "header"> + ${msg("loginProfileTitle")} + <#elseif section = "form"> +
+ + <@userProfileCommons.userProfileFormFields/> + +
+
+
+
+
+ +
+ <#if isAppInitiatedAction??> + + + <#else> + + +
+
+
+ + \ No newline at end of file diff --git a/themes/src/main/resources/theme/unixdog/login/user-profile-commons.ftl b/themes/src/main/resources/theme/unixdog/login/user-profile-commons.ftl new file mode 100644 index 0000000000..140eea31a8 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/user-profile-commons.ftl @@ -0,0 +1,187 @@ +<#macro userProfileFormFields> + <#assign currentGroup=""> + + <#list profile.attributes as attribute> + + <#assign groupName = attribute.group!""> + <#if groupName != currentGroup> + <#assign currentGroup=groupName> + <#if currentGroup != "" > +
+ + <#assign groupDisplayHeader=attribute.groupDisplayHeader!""> + <#if groupDisplayHeader != ""> + <#assign groupHeaderText=advancedMsg(attribute.groupDisplayHeader)!groupName> + <#else> + <#assign groupHeaderText=groupName> + +
+ +
+ + <#assign groupDisplayDescription=attribute.groupDisplayDescription!""> + <#if groupDisplayDescription != ""> + <#assign groupDescriptionText=advancedMsg(attribute.groupDisplayDescription)!""> +
+ +
+ +
+ + + + <#nested "beforeField" attribute> +
+
+ + <#if attribute.required>* +
+
+ <#if attribute.annotations.inputHelperTextBefore??> +
${kcSanitize(advancedMsg(attribute.annotations.inputHelperTextBefore))?no_esc}
+ + <@inputFieldByType attribute=attribute/> + <#if messagesPerField.existsError('${attribute.name}')> + + ${kcSanitize(messagesPerField.get('${attribute.name}'))?no_esc} + + + <#if attribute.annotations.inputHelperTextAfter??> +
${kcSanitize(advancedMsg(attribute.annotations.inputHelperTextAfter))?no_esc}
+ +
+
+ <#nested "afterField" attribute> + + + +<#macro inputFieldByType attribute> + <#switch attribute.annotations.inputType!''> + <#case 'textarea'> + <@textareaTag attribute=attribute/> + <#break> + <#case 'select'> + <#case 'multiselect'> + <@selectTag attribute=attribute/> + <#break> + <#case 'select-radiobuttons'> + <#case 'multiselect-checkboxes'> + <@inputTagSelects attribute=attribute/> + <#break> + <#default> + <@inputTag attribute=attribute/> + + + +<#macro inputTag attribute> + disabled + <#if attribute.autocomplete??>autocomplete="${attribute.autocomplete}" + <#if attribute.annotations.inputTypePlaceholder??>placeholder="${attribute.annotations.inputTypePlaceholder}" + <#if attribute.annotations.inputTypePattern??>pattern="${attribute.annotations.inputTypePattern}" + <#if attribute.annotations.inputTypeSize??>size="${attribute.annotations.inputTypeSize}" + <#if attribute.annotations.inputTypeMaxlength??>maxlength="${attribute.annotations.inputTypeMaxlength}" + <#if attribute.annotations.inputTypeMinlength??>minlength="${attribute.annotations.inputTypeMinlength}" + <#if attribute.annotations.inputTypeMax??>max="${attribute.annotations.inputTypeMax}" + <#if attribute.annotations.inputTypeMin??>min="${attribute.annotations.inputTypeMin}" + <#if attribute.annotations.inputTypeStep??>step="${attribute.annotations.inputTypeStep}" + /> + + +<#macro inputTagType attribute> + <#compress> + <#if attribute.annotations.inputType??> + <#if attribute.annotations.inputType?starts_with("html5-")> + ${attribute.annotations.inputType[6..]} + <#else> + ${attribute.annotations.inputType} + + <#else> + text + + + + +<#macro textareaTag attribute> + + + +<#macro selectTag attribute> + + + +<#macro inputTagSelects attribute> + <#if attribute.annotations.inputType=='select-radiobuttons'> + <#assign inputType='radio'> + <#assign classDiv=properties.kcInputClassRadio!> + <#assign classInput=properties.kcInputClassRadioInput!> + <#assign classLabel=properties.kcInputClassRadioLabel!> + <#else> + <#assign inputType='checkbox'> + <#assign classDiv=properties.kcInputClassCheckbox!> + <#assign classInput=properties.kcInputClassCheckboxInput!> + <#assign classLabel=properties.kcInputClassCheckboxLabel!> + + + <#if attribute.annotations.inputOptionsFromValidation?? && attribute.validators[attribute.annotations.inputOptionsFromValidation]?? && attribute.validators[attribute.annotations.inputOptionsFromValidation].options??> + <#assign options=attribute.validators[attribute.annotations.inputOptionsFromValidation].options> + <#elseif attribute.validators.options?? && attribute.validators.options.options??> + <#assign options=attribute.validators.options.options> + + + <#if options??> + <#list options as option> +
+ disabled + <#if attribute.values?seq_contains(option)>checked + /> + +
+ + + + + +<#macro selectOptionLabelText attribute option> + <#compress> + <#if attribute.annotations.inputOptionLabels??> + ${advancedMsg(attribute.annotations.inputOptionLabels[option]!option)} + <#else> + <#if attribute.annotations.inputOptionLabelsI18nPrefix??> + ${msg(attribute.annotations.inputOptionLabelsI18nPrefix + '.' + option)} + <#else> + ${option} + + + + \ No newline at end of file diff --git a/themes/src/main/resources/theme/unixdog/login/webauthn-authenticate.ftl b/themes/src/main/resources/theme/unixdog/login/webauthn-authenticate.ftl new file mode 100644 index 0000000000..a07f7503d8 --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/webauthn-authenticate.ftl @@ -0,0 +1,168 @@ + <#import "template.ftl" as layout> + <@layout.registrationLayout; section> + <#if section = "title"> + title + <#elseif section = "header"> + ${kcSanitize(msg("webauthn-login-title"))?no_esc} + <#elseif section = "form"> +
+
+ + + + + + +
+ +
+ <#if authenticators??> +
+ <#list authenticators.authenticators as authenticator> + + +
+ + <#if shouldDisplayAuthenticators?? && shouldDisplayAuthenticators> + <#if authenticators.authenticators?size gt 1> +

${kcSanitize(msg("webauthn-available-authenticators"))?no_esc}

+ + +
+ <#list authenticators.authenticators as authenticator> +
+
+ +
+
+
+ ${kcSanitize(msg('${authenticator.label}'))?no_esc} +
+ + <#if authenticator.transports?? && authenticator.transports.displayNameProperties?has_content> +
+ <#list authenticator.transports.displayNameProperties as nameProperty> + ${kcSanitize(msg('${nameProperty!}'))?no_esc} + <#if nameProperty?has_next> + , + + +
+ + +
+ + ${kcSanitize(msg('webauthn-createdAt-label'))?no_esc} + + + ${kcSanitize(authenticator.createdAt)?no_esc} + +
+
+
+
+ +
+ + + +
+ +
+
+
+ + + + + <#elseif section = "info"> + + + diff --git a/themes/src/main/resources/theme/unixdog/login/webauthn-error.ftl b/themes/src/main/resources/theme/unixdog/login/webauthn-error.ftl new file mode 100644 index 0000000000..2474a3f6cc --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/webauthn-error.ftl @@ -0,0 +1,36 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=true; section> + <#if section = "header"> + ${kcSanitize(msg("webauthn-error-title"))?no_esc} + <#elseif section = "form"> + + + +
+ + +
+ + + + <#if isAppInitiatedAction??> +
+ +
+ + + + \ No newline at end of file diff --git a/themes/src/main/resources/theme/unixdog/login/webauthn-register.ftl b/themes/src/main/resources/theme/unixdog/login/webauthn-register.ftl new file mode 100644 index 0000000000..021e8e91dc --- /dev/null +++ b/themes/src/main/resources/theme/unixdog/login/webauthn-register.ftl @@ -0,0 +1,194 @@ + <#import "template.ftl" as layout> + <@layout.registrationLayout; section> + <#if section = "title"> + title + <#elseif section = "header"> + ${kcSanitize(msg("webauthn-registration-title"))?no_esc} + <#elseif section = "form"> + +
+
+ + + + + + +
+
+ + + + + + + + + + <#if !isSetRetry?has_content && isAppInitiatedAction?has_content> +
+ +
+ + + +