diff --git a/README.md b/README.md index 052afc1..058e7c5 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Spring MVC: - favicon handler Security: -- Account management +- Account management with KeyCloak - Spring Security - User/User_Authority entity and repository/services - login, logout, home pages based on user role @@ -108,12 +108,12 @@ It contains following applications: # Note you will need to create a database named 'seedapp' in your mysql server -Option 1 - run with manually started ActiveMQ and MySQL servers +Option 1 - run with manually started KeyCloak, ActiveMQ and MySQL servers - Run ```mvn clean install``` at root - Run ```docker-compose -f config/docker-compose.yml up``` at root to start docker containers - Go to main-app folder and run ```mvn``` to start the application -Option 2 - automatically start ActiveMQ and MySQL using TestContainer while application is starting +Option 2 - automatically start KeyCloak, ActiveMQ and MySQL using TestContainer while application is starting - Run ```mvn clean install``` at root - Go to main-app folder and run ```mvn -Pdev,withTestContainer``` to start the application diff --git a/config/docker-compose.yml b/config/docker-compose.yml index 510c852..923adde 100644 --- a/config/docker-compose.yml +++ b/config/docker-compose.yml @@ -38,6 +38,25 @@ services: - 9411:9411 networks: - seedappnet + keycloak: + image: 'quay.io/keycloak/keycloak:23.0.3' + container_name: keycloak + command: start-dev --import-realm + environment: + - KEYCLOAK_DB=dev-file + - KEYCLOAK_ADMIN=admin + - KEYCLOAK_ADMIN_PASSWORD=admin + - KEYCLOAK_FEATURES=scripts + - KEYCLOAK_HTTP_PORT=8080 + - KEYCLOAK_HTTPS_PORT=9443 + # entrypoint: /tmp/keycloak/config/docker-compose-entrypoint.sh --hostname host.docker.internal:8080 + volumes: + - ../main-app/main-webapp/src/main/resources/keycloak/:/opt/keycloak/data/import + ports: + - 8082:8080 + - 9443:9443 + networks: + - seedappnet volumes: esdata1: driver: local diff --git a/email/email-service/pom.xml b/email/email-service/pom.xml index dad184b..d56dd43 100755 --- a/email/email-service/pom.xml +++ b/email/email-service/pom.xml @@ -145,14 +145,6 @@ true - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - ${springdoc-openapi-ui.version} - true - - dev dev diff --git a/email/email-service/src/main/java/gt/mail/web/rest/HelloResource.java b/email/email-service/src/main/java/gt/mail/web/rest/HelloResource.java index 44a5f43..f9d6994 100755 --- a/email/email-service/src/main/java/gt/mail/web/rest/HelloResource.java +++ b/email/email-service/src/main/java/gt/mail/web/rest/HelloResource.java @@ -1,6 +1,6 @@ package gt.mail.web.rest; -import gt.common.config.BaseException; +//import gt.common.config.BaseException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; @@ -21,9 +21,9 @@ public class HelloResource { @GetMapping("/hello") public Map sayHello() { log.info("Received hello request"); - if (RANDOM.nextBoolean()) { - throw new BaseException("Something"); - } +// if (RANDOM.nextBoolean()) { +// throw new BaseException("Something"); +// } return Map.of("hello", "world"); } } diff --git a/email/email-service/src/test/java/gt/mail/web/rest/EmailResourceIT.java b/email/email-service/src/test/java/gt/mail/web/rest/EmailResourceIT.java index 58084e4..f2b74bf 100644 --- a/email/email-service/src/test/java/gt/mail/web/rest/EmailResourceIT.java +++ b/email/email-service/src/test/java/gt/mail/web/rest/EmailResourceIT.java @@ -17,16 +17,27 @@ class EmailResourceIT { @Test - void sayHello2(@Autowired MockMvc mvc) throws Exception { + void canSendEmail(@Autowired MockMvc mvc) throws Exception { mvc.perform( - post("/sendEmail") - .contentType(MediaType.APPLICATION_JSON) - .content(sampleEmail().getBytes())) + post("/sendEmail") + .contentType(MediaType.APPLICATION_JSON) + .content(sampleEmail().getBytes())) .andExpect(status().isOk()); } String sampleEmail() { - return "{\"from\":\"test@email.com\",\"subject\":\"Test\",\"content\":\"Body\",\"isHtml\":false,\"files\":[],\"to\":[\"recep@emai.com\"],\"cc\":[],\"bcc\":[]}"; + return """ + { + "fromEmail":"test@email.com", + "subject":"Test", + "content":"Body", + "isHtml":false, + "files":[], + "to":["recep@emai.com"] + ,"cc":[], + "bcc":[] + } + """; } } diff --git a/email/email-service/src/test/resources/application.yml b/email/email-service/src/test/resources/application.yml index 5f981db..73251db 100755 --- a/email/email-service/src/test/resources/application.yml +++ b/email/email-service/src/test/resources/application.yml @@ -12,7 +12,11 @@ spring: port: ${MAILHOG_PORT:1025} username: password: - + security: + oauth2: + resourceserver: + jwt: + issuer-uri: http://localhost:${KEYCLOAK_PORT:8082}/realms/articleapp logging.level: ROOT: WARN gt: INFO diff --git a/main-app/main-orm/pom.xml b/main-app/main-orm/pom.xml index 204c7f4..86cb2ed 100644 --- a/main-app/main-orm/pom.xml +++ b/main-app/main-orm/pom.xml @@ -98,10 +98,15 @@ src/main/resources/liquibase-jpa-diff.properties - + + + org.liquibase.ext + liquibase-hibernate6 + ${liquibase.version} + org.springframework spring-beans diff --git a/main-app/main-orm/src/main/java/gt/app/domain/AppUser.java b/main-app/main-orm/src/main/java/gt/app/domain/AppUser.java index af9e314..71762f5 100644 --- a/main-app/main-orm/src/main/java/gt/app/domain/AppUser.java +++ b/main-app/main-orm/src/main/java/gt/app/domain/AppUser.java @@ -1,225 +1,61 @@ package gt.app.domain; -import jakarta.persistence.*; -import jakarta.validation.constraints.Size; -import lombok.EqualsAndHashCode; -import org.hibernate.annotations.BatchSize; -import org.hibernate.annotations.Fetch; -import org.hibernate.annotations.FetchMode; -import org.springframework.security.core.userdetails.UserDetails; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; -import java.util.Collection; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; +import java.util.UUID; -@EqualsAndHashCode(callSuper = true) @Entity +@Getter +@Setter @Table(name = "APP_USER") -public class AppUser extends BaseEntity implements UserDetails { - @Basic(fetch = FetchType.LAZY) - @Lob - byte[] avatar; +public class AppUser { + + @Id + @JdbcTypeCode(SqlTypes.BINARY) + UUID id; @Column(nullable = false) - @Size(min = 2, max = 30) private String firstName; - @Size(max = 30) private String lastName; - @Column(length = 254, unique = true, nullable = false) - private String email; - - @Column(nullable = false, unique = true) - @Size(min = 5, max = 20) - private String username; - - @Column(name = "password_hash", length = 60) - private String password; - - @ManyToMany(fetch = FetchType.LAZY) - @JoinTable( - name = "user_authority", - joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, - inverseJoinColumns = {@JoinColumn(name = "authority_name", referencedColumnName = "name")}) - @Fetch(FetchMode.SUBSELECT) - @BatchSize(size = 5) - private Set authorities = new HashSet<>(); - @Column(nullable = false) - private Boolean active = false; + private String username; //doesn't need to be unique (multi-tenacy) - @Column(nullable = false) - private Boolean accountNonExpired; @Column(nullable = false) - private Boolean accountNonLocked; - - @Column(nullable = false) - private Boolean credentialsNonExpired; - - private String activationKey; - - private String resetKey; - - @Override - public Collection getAuthorities() { - return authorities; - } - - @Override - public String getPassword() { - return password; - } - - @Override - public String getUsername() { - return username; - } - - @Override - public boolean isAccountNonExpired() { - return accountNonExpired; - } - - @Override - public boolean isAccountNonLocked() { - return accountNonLocked; - } - - @Override - public boolean isCredentialsNonExpired() { - return credentialsNonExpired; - } - - @Override - public boolean isEnabled() { - return active; - } - - public byte[] getAvatar() { - return avatar; - } - - public String getFirstName() { - return firstName; - } - - public String getLastName() { - return lastName; - } - - public String getEmail() { - return email; - } - - public String getActivationKey() { - return activationKey; - } - - public String getResetKey() { - return resetKey; - } - - public void setAvatar(byte[] avatar) { - this.avatar = avatar; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - - public void setEmail(String email) { - this.email = email; - } - - public void setUsername(String username) { - this.username = username; - } - - public void setPassword(String password) { - this.password = password; - } - - public void setAuthorities(Set authorities) { - this.authorities = authorities; - } - - public void setActive(Boolean active) { - this.active = active; - } - - public void setAccountNonExpired(Boolean accountNonExpired) { - this.accountNonExpired = accountNonExpired; - } - - public void setAccountNonLocked(Boolean accountNonLocked) { - this.accountNonLocked = accountNonLocked; - } - - public void setCredentialsNonExpired(Boolean credentialsNonExpired) { - this.credentialsNonExpired = credentialsNonExpired; - } - - public void setActivationKey(String activationKey) { - this.activationKey = activationKey; - } - - public void setResetKey(String resetKey) { - this.resetKey = resetKey; - } + private String email; //doesn't need to be unique (multi-tenacy) public AppUser() { } - public AppUser(String username, String firstName, String lastName, String email) { + public AppUser(UUID id, String username, String firstName, String lastName, String email) { + this.id = id; this.username = username; this.firstName = firstName; this.lastName = lastName; this.email = email; - this.accountNonExpired = Boolean.TRUE; - this.accountNonLocked = Boolean.TRUE; - this.credentialsNonExpired = Boolean.TRUE; - this.active = Boolean.TRUE; + } + + public AppUser(String id, String username, String firstName, String lastName, String email) { + this(UUID.fromString(id), username, firstName, lastName, email); } @Override public String toString() { return "User{" + + "id='" + id + '\'' + + "username='" + username + '\'' + "firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + - ", email='" + email + '\'' + - ", authorities=" + authorities + - ", active=" + active + - ", accountNonExpired=" + accountNonExpired + - ", accountNonLocked=" + accountNonLocked + - ", credentialsNonExpired=" + credentialsNonExpired + '}'; } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - if (!super.equals(o)) { - return false; - } - AppUser appUser = (AppUser) o; - return Objects.equals(username, appUser.username); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), username); - } } diff --git a/main-app/main-orm/src/main/java/gt/app/domain/LiteUser.java b/main-app/main-orm/src/main/java/gt/app/domain/LiteUser.java deleted file mode 100644 index 318e2ed..0000000 --- a/main-app/main-orm/src/main/java/gt/app/domain/LiteUser.java +++ /dev/null @@ -1,36 +0,0 @@ -package gt.app.domain; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import jakarta.validation.constraints.Size; -import lombok.Data; -import lombok.EqualsAndHashCode; - -@EqualsAndHashCode(callSuper = true) -@Entity -@Table(name="APP_USER") -@Data -public class LiteUser extends BaseEntity { - - @Column(nullable = false) - @Size(min = 2, max = 30) - private String firstName; - - @Size(max = 30) - private String lastName; - - @Column(length = 254, unique = true, nullable = false) - private String email; - - @Column(nullable = false, unique = true) - @Size(min = 5, max = 20) - private String username; - - @Column(name = "password_hash", length = 60) - private String password; - - @Column(nullable = false) - private Boolean active = false; - -} diff --git a/main-app/main-orm/src/main/resources/liquibase-jpa-diff.properties b/main-app/main-orm/src/main/resources/liquibase-jpa-diff.properties index 555799f..ab39387 100644 --- a/main-app/main-orm/src/main/resources/liquibase-jpa-diff.properties +++ b/main-app/main-orm/src/main/resources/liquibase-jpa-diff.properties @@ -5,7 +5,9 @@ driver=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/seedapp?allowPublicKeyRetrieval=true username=root password=password -referenceUrl=hibernate:spring:gt.app.domain?dialect=org.hibernate.dialect.MySQL8Dialect&hibernate.physical_naming_strategy=gt.app.hibernate.PrefixedNamingStrategy&hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy +referenceUrl=hibernate:spring:gt.app.domain?dialect=org.hibernate.dialect.MySQLDialect&hibernate.physical_naming_strategy=gt.app.hibernate.PrefixedNamingStrategy&hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy +#referenceUrl=hibernate:spring:gt.app.domain?dialect=${liquibase-plugin.hibernate-dialect}&hibernate.physical_naming_strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy&hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy ## the default naming strategies are configured here: org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties.Naming.applyNamingStrategies ## we have to map the same for liquibase so that it will generate the column/tables that's compatible with Spring-Data-JPA + diff --git a/main-app/main-orm/src/main/resources/liquibase/changelog/20221112224444_changelog.xml b/main-app/main-orm/src/main/resources/liquibase/changelog/20221112224444_changelog.xml deleted file mode 100644 index 1030c8c..0000000 --- a/main-app/main-orm/src/main/resources/liquibase/changelog/20221112224444_changelog.xml +++ /dev/null @@ -1,187 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/main-app/main-orm/src/main/resources/liquibase/changelog/20231231191733_changelog.xml b/main-app/main-orm/src/main/resources/liquibase/changelog/20231231191733_changelog.xml new file mode 100644 index 0000000..af6c8b4 --- /dev/null +++ b/main-app/main-orm/src/main/resources/liquibase/changelog/20231231191733_changelog.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/main-app/main-orm/src/main/resources/liquibase/master.xml b/main-app/main-orm/src/main/resources/liquibase/master.xml index 34c6b5f..fbcb948 100644 --- a/main-app/main-orm/src/main/resources/liquibase/master.xml +++ b/main-app/main-orm/src/main/resources/liquibase/master.xml @@ -4,6 +4,6 @@ xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.6.xsd"> - + diff --git a/main-app/main-webapp/pom.xml b/main-app/main-webapp/pom.xml index 05084a5..6ff0447 100644 --- a/main-app/main-webapp/pom.xml +++ b/main-app/main-webapp/pom.xml @@ -136,7 +136,15 @@ org.springframework.boot spring-boot-starter-security - + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + @@ -226,6 +234,11 @@ + + com.github.dasniko + testcontainers-keycloak + true + org.testcontainers testcontainers @@ -236,7 +249,6 @@ junit-jupiter test - org.testcontainers mysql @@ -531,6 +543,10 @@ org.testcontainers testcontainers + + com.github.dasniko + testcontainers-keycloak + org.testcontainers mysql diff --git a/main-app/main-webapp/src/main/java/gt/app/DataCreator.java b/main-app/main-webapp/src/main/java/gt/app/DataCreator.java index 45d131d..089f550 100644 --- a/main-app/main-webapp/src/main/java/gt/app/DataCreator.java +++ b/main-app/main-webapp/src/main/java/gt/app/DataCreator.java @@ -15,6 +15,7 @@ import org.springframework.stereotype.Component; import java.io.File; +import java.util.UUID; import java.util.stream.Stream; @Component @@ -64,21 +65,22 @@ public void initData() { userAuthority.setName(Constants.ROLE_USER); authorityService.save(userAuthority); - String pwd = "$2a$10$UtqWHf0BfCr41Nsy89gj4OCiL36EbTZ8g4o/IvFN2LArruHruiRXO"; // to make it faster //value is 'pass' + String systemUserId = "a621ac4c-6172-4103-9050-b27c053b11eb"; - AppUser adminUser = new AppUser(SYSTEM_USER, "System", "Tiwari", "system@email"); - adminUser.setPassword(pwd); - adminUser.setAuthorities(authorityService.findByNameIn(Constants.ROLE_ADMIN, Constants.ROLE_USER)); + if (userService.exists(UUID.fromString(systemUserId))) { + log.info("DB already initialized !!!"); + return; + } + + //ID and login are linked with the keycloak export json + AppUser adminUser = new AppUser(systemUserId, "system", "System", "Tiwari", "system@email"); userService.save(adminUser); - AppUser user1 = new AppUser(USER1, "Ganesh", "Tiwari", "gt@email"); - user1.setPassword(pwd); - user1.setAuthorities(authorityService.findByNameIn(Constants.ROLE_USER)); + AppUser user1 = new AppUser("d1460f56-7f7e-43e1-8396-bddf39dba08f", "user1", "Ganesh", "Tiwari", "user1@email"); userService.save(user1); - AppUser user2 = new AppUser(USER2, "Jyoti", "Kattel", "jk@email"); - user2.setPassword(pwd); - user2.setAuthorities(authorityService.findByNameIn(Constants.ROLE_USER)); + + AppUser user2 = new AppUser("fa6820a5-cf39-4cbf-9e50-89cc832bebee", "user2", "Jyoti", "Kattel", "user2@email"); userService.save(user2); createArticle(adminUser, "Admin's First Article", "Content1 Admin"); diff --git a/main-app/main-webapp/src/main/java/gt/app/api/EmailClient.java b/main-app/main-webapp/src/main/java/gt/app/api/EmailClient.java index 228f198..6be0c04 100644 --- a/main-app/main-webapp/src/main/java/gt/app/api/EmailClient.java +++ b/main-app/main-webapp/src/main/java/gt/app/api/EmailClient.java @@ -9,6 +9,7 @@ import org.springframework.http.ResponseEntity; @FeignClient(name = "email-service", url = "${feign-clients.email-service.url}", fallback = EmailClient.EmailClientFallback.class) +//doesn't use auth token public interface EmailClient extends EmailService { @Slf4j class EmailClientFallback implements EmailClient { diff --git a/main-app/main-webapp/src/main/java/gt/app/api/InternalKeycloakAuthConfig.java b/main-app/main-webapp/src/main/java/gt/app/api/InternalKeycloakAuthConfig.java new file mode 100644 index 0000000..a70007c --- /dev/null +++ b/main-app/main-webapp/src/main/java/gt/app/api/InternalKeycloakAuthConfig.java @@ -0,0 +1,39 @@ +package gt.app.api; + +import feign.RequestInterceptor; +import gt.app.config.security.SecurityUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; + +import java.util.Optional; + +@Slf4j +class InternalKeycloakAuthConfig { + + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String BEARER = "Bearer"; + + + @Bean + public RequestInterceptor bearerAuthRequestInterceptor() { + return template -> { + //NOTE: pass the tokens only to the app that expects it + + Optional idTokenOpt = SecurityUtils.getCurrentUserJWT(); + if (idTokenOpt.isEmpty()) { + return; + } + + /* + //get new token if its about to expire + if (idTokenOpt.get().getExpiresAt().isAfter(Instant.now().plus(20, ChronoUnit.SECONDS))) { + //TODO: refresh token + } + */ + log.info("Calling {} with token", template.url()); + template.header(AUTHORIZATION_HEADER, String.format("%s %s", BEARER, idTokenOpt.get().getTokenValue())); + }; + } + +} diff --git a/main-app/main-webapp/src/main/java/gt/app/api/ReportClient.java b/main-app/main-webapp/src/main/java/gt/app/api/ReportClient.java new file mode 100644 index 0000000..486df19 --- /dev/null +++ b/main-app/main-webapp/src/main/java/gt/app/api/ReportClient.java @@ -0,0 +1,27 @@ +package gt.app.api; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; + +@FeignClient(name = "report-service", url = "${feign-clients.report-service.url}", fallback = ReportClient.ReportClientFallback.class, configuration = InternalKeycloakAuthConfig.class) +public interface ReportClient { + @PostMapping(value = "/to-review", produces = MediaType.APPLICATION_JSON_VALUE) + FlagCount getFlaggedForReviewCount(); + + @Slf4j + class ReportClientFallback implements ReportClient { + + + @Override + public FlagCount getFlaggedForReviewCount() { + return new FlagCount(-100); + } + } + record FlagCount(int value) { + } +} + + + diff --git a/main-app/main-webapp/src/main/java/gt/app/config/AppProperties.java b/main-app/main-webapp/src/main/java/gt/app/config/AppProperties.java index ca932db..1fed24d 100644 --- a/main-app/main-webapp/src/main/java/gt/app/config/AppProperties.java +++ b/main-app/main-webapp/src/main/java/gt/app/config/AppProperties.java @@ -9,6 +9,8 @@ public class AppProperties { final FileStorage fileStorage = new FileStorage(); final JmsProps jms = new JmsProps(); + final Web web = new Web(); + final Email email = new Email(); @Data @@ -22,6 +24,14 @@ public static class JmsProps { String contentCheckerCallBackResponseQueue; } + @Data + public static class Web { + /** + * the registered host.. without trailing / + */ + String baseUrl; + } + @Data public static class Email { String authorNotificationsFromEmail = "no-reply@system"; diff --git a/main-app/main-webapp/src/main/java/gt/app/config/DockerContainerConfig.java b/main-app/main-webapp/src/main/java/gt/app/config/DockerContainerConfig.java index 3c46589..4249ee8 100644 --- a/main-app/main-webapp/src/main/java/gt/app/config/DockerContainerConfig.java +++ b/main-app/main-webapp/src/main/java/gt/app/config/DockerContainerConfig.java @@ -1,5 +1,6 @@ package gt.app.config; +import dasniko.testcontainers.keycloak.KeycloakContainer; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; @@ -39,6 +40,11 @@ class DockerContainerConfig { activeMQ.withExposedPorts(61616); activeMQ.start(); //using default ports + var kc = new KeycloakContainer("quay.io/keycloak/keycloak:23.0.3").withRealmImportFile("keycloak/realm-export.json"); + kc.start(); + + setProperty("KEYCLOAK_PORT", Integer.toString(kc.getHttpPort())); + setProperty("ACTIVEMQ_ARTEMIS_HOST", activeMQ.getHost()); setProperty("ACTIVEMQ_ARTEMIS_PORT", Integer.toString(activeMQ.getMappedPort(61616))); setProperty("ACTIVEMQ_ARTEMIS_USER", userPwd); diff --git a/main-app/main-webapp/src/main/java/gt/app/config/security/AppUserDetails.java b/main-app/main-webapp/src/main/java/gt/app/config/security/AppUserDetails.java index dd346a3..e85ab9f 100644 --- a/main-app/main-webapp/src/main/java/gt/app/config/security/AppUserDetails.java +++ b/main-app/main-webapp/src/main/java/gt/app/config/security/AppUserDetails.java @@ -1,68 +1,68 @@ -package gt.app.config.security; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import gt.app.config.Constants; -import gt.app.domain.Authority; -import lombok.Getter; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.User; - -import java.util.Collection; -import java.util.Objects; -import java.util.stream.Collectors; - -@Getter -public class AppUserDetails extends User { - - private final Long id; - private final String firstName; - - private final String lastName; - private final String email; - - public AppUserDetails(Long id, String userName, String email, String password, String firstName, String lastName, Collection authorities, - boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked) { - - super(userName, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); - - this.id = id; - this.firstName = firstName; - this.lastName = lastName; - this.email = email; - } - - @JsonIgnore - public boolean isUser() { - return getGrantedAuthorities().contains(Constants.ROLE_USER); - } - - @JsonIgnore - public boolean isSystemAdmin() { - return getGrantedAuthorities().contains(Constants.ROLE_ADMIN); - } - - public Collection getGrantedAuthorities() { - Collection authorities = getAuthorities(); - return authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet()); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - if (!super.equals(o)) { - return false; - } - AppUserDetails that = (AppUserDetails) o; - return id.equals(that.id); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), id); - } -} +//package gt.app.config.security; +// +//import com.fasterxml.jackson.annotation.JsonIgnore; +//import gt.app.config.Constants; +//import gt.app.domain.Authority; +//import lombok.Getter; +//import org.springframework.security.core.GrantedAuthority; +//import org.springframework.security.core.userdetails.User; +// +//import java.util.Collection; +//import java.util.Objects; +//import java.util.stream.Collectors; +// +//@Getter +//public class AppUserDetails extends User { +// +// private final Long id; +// private final String firstName; +// +// private final String lastName; +// private final String email; +// +// public AppUserDetails(Long id, String userName, String email, String password, String firstName, String lastName, Collection authorities, +// boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked) { +// +// super(userName, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); +// +// this.id = id; +// this.firstName = firstName; +// this.lastName = lastName; +// this.email = email; +// } +// +// @JsonIgnore +// public boolean isUser() { +// return getGrantedAuthorities().contains(Constants.ROLE_USER); +// } +// +// @JsonIgnore +// public boolean isSystemAdmin() { +// return getGrantedAuthorities().contains(Constants.ROLE_ADMIN); +// } +// +// public Collection getGrantedAuthorities() { +// Collection authorities = getAuthorities(); +// return authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet()); +// } +// +// @Override +// public boolean equals(Object o) { +// if (this == o) { +// return true; +// } +// if (o == null || getClass() != o.getClass()) { +// return false; +// } +// if (!super.equals(o)) { +// return false; +// } +// AppUserDetails that = (AppUserDetails) o; +// return id.equals(that.id); +// } +// +// @Override +// public int hashCode() { +// return Objects.hash(super.hashCode(), id); +// } +//} diff --git a/main-app/main-webapp/src/main/java/gt/app/config/security/CurrentUser.java b/main-app/main-webapp/src/main/java/gt/app/config/security/CurrentUser.java new file mode 100644 index 0000000..f867334 --- /dev/null +++ b/main-app/main-webapp/src/main/java/gt/app/config/security/CurrentUser.java @@ -0,0 +1,13 @@ +package gt.app.config.security; + + +import org.springframework.security.core.annotation.AuthenticationPrincipal; + +import java.lang.annotation.*; + +@Target({ElementType.PARAMETER, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@AuthenticationPrincipal(errorOnInvalidType = true, expression = "T(gt.app.config.security.SecurityUtils).mapAuthenticationPrincipalToCurrentUser(#this)") +public @interface CurrentUser { +} diff --git a/main-app/main-webapp/src/main/java/gt/app/config/security/CurrentUserToken.java b/main-app/main-webapp/src/main/java/gt/app/config/security/CurrentUserToken.java new file mode 100644 index 0000000..c52bfef --- /dev/null +++ b/main-app/main-webapp/src/main/java/gt/app/config/security/CurrentUserToken.java @@ -0,0 +1,90 @@ +package gt.app.config.security; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import gt.app.config.Constants; +import lombok.Getter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; + +import java.io.Serial; +import java.util.Collection; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; + +@Getter +public class CurrentUserToken extends DefaultOidcUser { + + @Serial + private static final long serialVersionUID = 332175240400944267L; + private final Collection authorities; + private final UUID userId; + private final String username; + private final String email; + + public CurrentUserToken(Collection authorities, OidcIdToken idToken, OidcUserInfo userInfo) { + super(authorities, idToken, userInfo); + this.authorities = SecurityUtils.extractAuthorityFromClaims(userInfo.getClaims()); + this.userId = UUID.fromString(getName()); + this.username = getAttribute("preferred_username"); + this.email = getAttribute("email"); + } + + public CurrentUserToken(Collection authorities, String username) { + super(authorities, OidcIdToken.withTokenValue("DUMMY-TEST").claim("DUMMY", "DUMMY").claim("sub", username).build()); + this.authorities = authorities; + this.userId = null; + this.username = username; + this.email = null; + } + + @JsonIgnore + public boolean isUser() { + return getGrantedAuthorities().contains(Constants.ROLE_USER); + } + + @JsonIgnore + public boolean isAdmin() { + return getGrantedAuthorities().contains(Constants.ROLE_ADMIN); + } + + @JsonIgnore + public Collection getGrantedAuthorities() { + Collection authorities = getAuthorities(); + return authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet()); + } + + @JsonIgnore + public Collection getAuthorities() { + return authorities; + } + + public UserToken getUserToken() { + return new UserToken(getUsername(), getIdToken().getTokenValue(), getGrantedAuthorities()); + } + + public record UserToken(String username, String token, Collection roles) { + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + CurrentUserToken that = (CurrentUserToken) o; + return Objects.equals(userId, that.userId); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), userId); + } +} diff --git a/main-app/main-webapp/src/main/java/gt/app/config/security/SecurityAuditorResolver.java b/main-app/main-webapp/src/main/java/gt/app/config/security/SecurityAuditorResolver.java index 1f4c5d7..e546de1 100644 --- a/main-app/main-webapp/src/main/java/gt/app/config/security/SecurityAuditorResolver.java +++ b/main-app/main-webapp/src/main/java/gt/app/config/security/SecurityAuditorResolver.java @@ -1,23 +1,28 @@ package gt.app.config.security; import gt.app.domain.AppUser; -import gt.app.modules.user.UserService; +import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.AuditorAware; import org.springframework.stereotype.Component; import java.util.Optional; +import java.util.UUID; @Component @RequiredArgsConstructor class SecurityAuditorResolver implements AuditorAware { - private final UserService userService; + private final EntityManager entityManager; @Override public Optional getCurrentAuditor() { - Optional userLogin = SecurityUtils.getCurrentUserId(); - return userLogin.map(userService::getReference); + UUID userId = SecurityUtils.getCurrentUserId(); + if (userId == null) { + return Optional.empty(); + } + + return Optional.ofNullable(entityManager.getReference(AppUser.class, userId)); } } diff --git a/main-app/main-webapp/src/main/java/gt/app/config/security/SecurityConfig.java b/main-app/main-webapp/src/main/java/gt/app/config/security/SecurityConfig.java index 453f3bf..0e40c71 100644 --- a/main-app/main-webapp/src/main/java/gt/app/config/security/SecurityConfig.java +++ b/main-app/main-webapp/src/main/java/gt/app/config/security/SecurityConfig.java @@ -2,6 +2,7 @@ import gt.app.config.Constants; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @@ -10,10 +11,17 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtDecoders; import org.springframework.security.web.SecurityFilterChain; +import java.util.List; +import java.util.stream.Collectors; + +import static org.springframework.security.config.Customizer.withDefaults; + @EnableWebSecurity @EnableMethodSecurity(securedEnabled = true) @Configuration @@ -46,22 +54,28 @@ protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers(AUTH_WHITELIST).permitAll() .requestMatchers("/admin/**").hasAuthority(Constants.ROLE_ADMIN) .requestMatchers("/user/**").hasAuthority(Constants.ROLE_USER) + .requestMatchers("/auth/login").permitAll() .requestMatchers("/api/**").authenticated()//individual api will be secured differently .anyRequest().authenticated()) //this one will catch the rest patterns .csrf(AbstractHttpConfigurer::disable) - .formLogin(f -> f - .loginProcessingUrl("/auth/login") - .permitAll()) - .logout(l -> l - .logoutUrl("/auth/logout") - .logoutSuccessUrl("/?logout") - .permitAll()); + .oauth2Login(withDefaults()) + .oauth2ResourceServer(o2 -> o2.jwt(withDefaults())) + .oauth2Client(withDefaults()); return http.build(); } @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); + JwtDecoder jwtDecoder(@Value("${spring.security.oauth2.client.provider.oidc.issuer-uri}") String issuerUri) { + return JwtDecoders.fromOidcIssuerLocation(issuerUri); + } + + @Bean + public GrantedAuthoritiesMapper userAuthoritiesMapper() { + return authorities -> authorities.stream().filter(a -> a instanceof OidcUserAuthority) + .map(a -> (OidcUserAuthority) a) + .map(a -> SecurityUtils.extractAuthorityFromClaims(a.getUserInfo().getClaims())) + .flatMap(List::stream) + .collect(Collectors.toSet()); } } diff --git a/main-app/main-webapp/src/main/java/gt/app/config/security/SecurityUtils.java b/main-app/main-webapp/src/main/java/gt/app/config/security/SecurityUtils.java index b02e5c5..d080ada 100644 --- a/main-app/main-webapp/src/main/java/gt/app/config/security/SecurityUtils.java +++ b/main-app/main-webapp/src/main/java/gt/app/config/security/SecurityUtils.java @@ -1,32 +1,73 @@ package gt.app.config.security; +import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.User; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; -import java.util.Optional; +import java.util.*; +import java.util.stream.Collectors; public final class SecurityUtils { - public static Optional getCurrentUserId() { + private SecurityUtils() { + } + + public static UUID getCurrentUserId() { + + CurrentUserToken user = getCurrentUserDetails(); + if (user == null) { + return null; + } + + return user.getUserId(); + } + + public static CurrentUserToken getCurrentUserDetails() { + SecurityContext ctx = SecurityContextHolder.getContext(); + Authentication authentication = ctx.getAuthentication(); + if (authentication == null) { + return null; + } - User user = getCurrentUserDetails(); - if (user instanceof AppUserDetails appUserDetails) { - return Optional.of(appUserDetails.getId()); + if (authentication.getPrincipal() instanceof DefaultOidcUser defaultOidcUser) { + return mapAuthenticationPrincipalToCurrentUser(defaultOidcUser); } - return Optional.empty(); + return null; + } + + public static Optional getCurrentUserJWT() { + SecurityContext securityContext = SecurityContextHolder.getContext(); + return Optional + .ofNullable(securityContext.getAuthentication()) + .filter(auth -> auth.getPrincipal() instanceof DefaultOidcUser) + .map(auth -> (DefaultOidcUser) auth.getPrincipal()) + .map(DefaultOidcUser::getIdToken); } - public static User getCurrentUserDetails() { - var authentication = SecurityContextHolder.getContext().getAuthentication(); - return getCurrentUserDetails(authentication); + public static boolean isAuthenticated() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return !(authentication == null || authentication instanceof AnonymousAuthenticationToken); } - public static User getCurrentUserDetails(Authentication authentication) { - User userDetails = null; - if (authentication != null && authentication.getPrincipal() instanceof User) { - userDetails = (User) authentication.getPrincipal(); + public static List mapRolesToGrantedAuthorities(Collection roles) { + if (roles == null) { + return Collections.emptyList(); } - return userDetails; + return roles.stream() + .filter(role -> role.startsWith("ROLE_")) + .map(SimpleGrantedAuthority::new).collect(Collectors.toList()); + } + + public static List extractAuthorityFromClaims(Map claims) { + return mapRolesToGrantedAuthorities((ArrayList) claims.get("roles")); + } + + public static CurrentUserToken mapAuthenticationPrincipalToCurrentUser(DefaultOidcUser oidcUser) { + return new CurrentUserToken(oidcUser.getAuthorities(), oidcUser.getIdToken(), oidcUser.getUserInfo()); } } diff --git a/main-app/main-webapp/src/main/java/gt/app/modules/article/ArticlePreviewDto.java b/main-app/main-webapp/src/main/java/gt/app/modules/article/ArticlePreviewDto.java index d63204d..76c3484 100644 --- a/main-app/main-webapp/src/main/java/gt/app/modules/article/ArticlePreviewDto.java +++ b/main-app/main-webapp/src/main/java/gt/app/modules/article/ArticlePreviewDto.java @@ -20,7 +20,7 @@ public class ArticlePreviewDto { private String content; - private Long userId; + private UUID userId; private String username; private Instant createdDate; diff --git a/main-app/main-webapp/src/main/java/gt/app/modules/article/ArticleReadDto.java b/main-app/main-webapp/src/main/java/gt/app/modules/article/ArticleReadDto.java index 63afdd7..196f6ad 100644 --- a/main-app/main-webapp/src/main/java/gt/app/modules/article/ArticleReadDto.java +++ b/main-app/main-webapp/src/main/java/gt/app/modules/article/ArticleReadDto.java @@ -21,7 +21,7 @@ public class ArticleReadDto { private String content; - private Long userId; + private UUID userId; private String username; private Instant createdDate; @@ -44,7 +44,7 @@ public static class CommentDto { Long id; String content; - Long userId; + UUID userId; String username; Instant createdDate; diff --git a/main-app/main-webapp/src/main/java/gt/app/modules/article/ArticleRepository.java b/main-app/main-webapp/src/main/java/gt/app/modules/article/ArticleRepository.java index 5d20ed3..32740af 100644 --- a/main-app/main-webapp/src/main/java/gt/app/modules/article/ArticleRepository.java +++ b/main-app/main-webapp/src/main/java/gt/app/modules/article/ArticleRepository.java @@ -13,6 +13,7 @@ import org.springframework.data.repository.query.Param; import java.util.Optional; +import java.util.UUID; public interface ArticleRepository extends AbstractRepository
, ArticleRepositoryCustom { @@ -32,10 +33,10 @@ public interface ArticleRepository extends AbstractRepository
, ArticleR Optional
findOneWithUserById(Long id); @EntityGraph(attributePaths = {"createdByUser", "attachedFiles"}) - Page
findWithFilesAndUserByCreatedByUser_IdAndStatusOrderByCreatedDateDesc(Long userId, ArticleStatus status, Pageable pageable); + Page
findWithFilesAndUserByCreatedByUser_IdAndStatusOrderByCreatedDateDesc(UUID userId, ArticleStatus status, Pageable pageable); @Query("select n.createdByUser.id from Article n where n.id=:id ") - Long findCreatedByUserIdById(@Param("id") Long id); + UUID findCreatedByUserIdById(@Param("id") Long id); @EntityGraph(attributePaths = {"createdByUser", "lastModifiedByUser"}) Optional
findWithModifiedUserByIdAndStatus(Long id, ArticleStatus flagged); @@ -44,6 +45,7 @@ public interface ArticleRepository extends AbstractRepository
, ArticleR @Caching( evict = { @CacheEvict(cacheNames = {"articleForReview", "articleRead"}, key = "#result.id"), + //FIXME: better cache eviction - evict only if the article being updated was cached @CacheEvict(cacheNames = {"previewForPublicHomePage", "previewAllWithFilesByUser", "getAllToReview"}, allEntries = true) } ) @@ -53,6 +55,7 @@ public interface ArticleRepository extends AbstractRepository
, ArticleR @Caching( evict = { @CacheEvict(cacheNames = {"articleForReview", "articleRead"}, key = "#id"), + //FIXME: better cache eviction - evict only if the article being deleted was cached @CacheEvict(cacheNames = {"previewForPublicHomePage", "previewAllWithFilesByUser", "getAllToReview"}, allEntries = true) } ) diff --git a/main-app/main-webapp/src/main/java/gt/app/modules/article/ArticleService.java b/main-app/main-webapp/src/main/java/gt/app/modules/article/ArticleService.java index f70efa5..8bb03a0 100644 --- a/main-app/main-webapp/src/main/java/gt/app/modules/article/ArticleService.java +++ b/main-app/main-webapp/src/main/java/gt/app/modules/article/ArticleService.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.UUID; @Service @Slf4j @@ -113,7 +114,7 @@ public Page previewForPublicHomePage(Pageable pageable) { } @Cacheable(cacheNames = "previewAllWithFilesByUser") - public Page previewAllWithFilesByUser(Pageable pageable, Long userId) { + public Page previewAllWithFilesByUser(Pageable pageable, UUID userId) { return articleRepository.findWithFilesAndUserByCreatedByUser_IdAndStatusOrderByCreatedDateDesc(userId, ArticleStatus.PUBLISHED, pageable) .map(ArticleMapper.INSTANCE::mapForPreviewListing); } @@ -137,7 +138,7 @@ public void delete(Long id) { } @Cacheable(cacheNames = {"article-findCreatedByUserIdById"}) - public Long findCreatedByUserIdById(Long articleId) { + public UUID findCreatedByUserIdById(Long articleId) { return articleRepository.findCreatedByUserIdById(articleId); } diff --git a/main-app/main-webapp/src/main/java/gt/app/modules/user/AppPermissionEvaluatorService.java b/main-app/main-webapp/src/main/java/gt/app/modules/user/AppPermissionEvaluatorService.java index 015a0b2..538dc4c 100644 --- a/main-app/main-webapp/src/main/java/gt/app/modules/user/AppPermissionEvaluatorService.java +++ b/main-app/main-webapp/src/main/java/gt/app/modules/user/AppPermissionEvaluatorService.java @@ -1,6 +1,6 @@ package gt.app.modules.user; -import gt.app.config.security.AppUserDetails; +import gt.app.config.security.CurrentUserToken; import gt.app.config.security.SecurityUtils; import gt.app.domain.BaseEntity; import gt.app.exception.OperationNotAllowedException; @@ -8,7 +8,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.core.Authentication; -import org.springframework.security.core.userdetails.User; import org.springframework.stereotype.Service; import java.io.Serializable; @@ -45,14 +44,15 @@ public boolean hasPermission(Authentication auth, Serializable targetId, String } public boolean hasAccess(Long id, String targetEntity) { - User curUser = SecurityUtils.getCurrentUserDetails(); + CurrentUserToken curUserOpt = SecurityUtils.getCurrentUserDetails(); - if (!(curUser instanceof AppUserDetails appUser)) { - throw new OperationNotAllowedException("Current SecurityContext doesn't have AppUserDetails "); + if (curUserOpt == null) { + return false; } - return userAuthorityService.hasAccess(appUser, id, targetEntity); + return userAuthorityService.hasAccess(curUserOpt, id, targetEntity); } + } diff --git a/main-app/main-webapp/src/main/java/gt/app/modules/user/AppUserDetailsService.java b/main-app/main-webapp/src/main/java/gt/app/modules/user/AppUserDetailsService.java index e05807b..6cc64ce 100644 --- a/main-app/main-webapp/src/main/java/gt/app/modules/user/AppUserDetailsService.java +++ b/main-app/main-webapp/src/main/java/gt/app/modules/user/AppUserDetailsService.java @@ -1,34 +1,34 @@ -package gt.app.modules.user; - -import gt.app.config.security.AppUserDetails; -import gt.app.domain.AppUser; -import lombok.AllArgsConstructor; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Optional; - -@Service -@AllArgsConstructor -public class AppUserDetailsService implements org.springframework.security.core.userdetails.UserDetailsService { - - private final UserRepository userRepository; - - @Override - public AppUserDetails loadUserByUsername(String username) { - Optional userFromDatabase = userRepository.findOneWithAuthoritiesByUsername(username); - - return userFromDatabase - .map(this::getCustomUserDetails) - .orElseThrow(() -> new UsernameNotFoundException(" User with login:" + username + " was not found in the " + " database ")); - } - - @Transactional(readOnly = true) - public AppUserDetails getCustomUserDetails(AppUser user) { - - return new AppUserDetails(user.getId(), user.getUsername(), user.getEmail(), user.getPassword(), user.getFirstName(), user.getLastName(), user.getAuthorities(), - user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked()); - } - -} +//package gt.app.modules.user; +// +//import gt.app.config.security.AppUserDetails; +//import gt.app.domain.AppUser; +//import lombok.AllArgsConstructor; +//import org.springframework.security.core.userdetails.UsernameNotFoundException; +//import org.springframework.stereotype.Service; +//import org.springframework.transaction.annotation.Transactional; +// +//import java.util.Optional; +// +//@Service +//@AllArgsConstructor +//public class AppUserDetailsService implements org.springframework.security.core.userdetails.UserDetailsService { +// +// private final UserRepository userRepository; +// +// @Override +// public AppUserDetails loadUserByUsername(String username) { +// Optional userFromDatabase = userRepository.findOneWithAuthoritiesByUsername(username); +// +// return userFromDatabase +// .map(this::getCustomUserDetails) +// .orElseThrow(() -> new UsernameNotFoundException(" User with login:" + username + " was not found in the " + " database ")); +// } +// +// @Transactional(readOnly = true) +// public AppUserDetails getCustomUserDetails(AppUser user) { +// +// return new AppUserDetails(user.getId(), user.getUsername(), user.getEmail(), user.getPassword(), user.getFirstName(), user.getLastName(), user.getAuthorities(), +// user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked()); +// } +// +//} diff --git a/main-app/main-webapp/src/main/java/gt/app/modules/user/LiteUserRepository.java b/main-app/main-webapp/src/main/java/gt/app/modules/user/LiteUserRepository.java deleted file mode 100644 index 33a081e..0000000 --- a/main-app/main-webapp/src/main/java/gt/app/modules/user/LiteUserRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package gt.app.modules.user; - -import gt.app.domain.LiteUser; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Optional; - -interface LiteUserRepository extends JpaRepository { - @Transactional(readOnly = true) - @Cacheable(value = "userByUsername", unless = "#result == null") - Optional findOneByUsername(String username); -} diff --git a/main-app/main-webapp/src/main/java/gt/app/modules/user/UserAuthorityService.java b/main-app/main-webapp/src/main/java/gt/app/modules/user/UserAuthorityService.java index 5ec64ef..6a7435f 100644 --- a/main-app/main-webapp/src/main/java/gt/app/modules/user/UserAuthorityService.java +++ b/main-app/main-webapp/src/main/java/gt/app/modules/user/UserAuthorityService.java @@ -1,6 +1,6 @@ package gt.app.modules.user; -import gt.app.config.security.AppUserDetails; +import gt.app.config.security.CurrentUserToken; import gt.app.domain.Article; import gt.app.modules.article.ArticleService; import lombok.RequiredArgsConstructor; @@ -8,6 +8,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.UUID; + @Service("appSecurity") @RequiredArgsConstructor @Slf4j @@ -16,18 +18,18 @@ public class UserAuthorityService { private final ArticleService articleService; - public boolean hasAccess(AppUserDetails curUser, Long id, String entity) { + public boolean hasAccess(CurrentUserToken curUser, Long id, String entity) { - if (curUser.isSystemAdmin()) { + if (curUser.isAdmin()) { return true; } if (Article.class.getSimpleName().equalsIgnoreCase(entity)) { - Long createdById = articleService.findCreatedByUserIdById(id); + UUID createdById = articleService.findCreatedByUserIdById(id); - return createdById.equals(curUser.getId()); + return createdById.equals(curUser.getUserId()); } diff --git a/main-app/main-webapp/src/main/java/gt/app/modules/user/UserRepository.java b/main-app/main-webapp/src/main/java/gt/app/modules/user/UserRepository.java index 9111874..b44f993 100644 --- a/main-app/main-webapp/src/main/java/gt/app/modules/user/UserRepository.java +++ b/main-app/main-webapp/src/main/java/gt/app/modules/user/UserRepository.java @@ -7,12 +7,13 @@ import org.springframework.transaction.annotation.Transactional; import java.util.Optional; +import java.util.UUID; -interface UserRepository extends JpaRepository { +interface UserRepository extends JpaRepository { @Override @Cacheable("userExistsById") - boolean existsById(Long id); + boolean existsById(UUID id); @EntityGraph(attributePaths = {"authorities"}) @Transactional(readOnly = true) diff --git a/main-app/main-webapp/src/main/java/gt/app/modules/user/UserService.java b/main-app/main-webapp/src/main/java/gt/app/modules/user/UserService.java index 345fe52..acf527c 100644 --- a/main-app/main-webapp/src/main/java/gt/app/modules/user/UserService.java +++ b/main-app/main-webapp/src/main/java/gt/app/modules/user/UserService.java @@ -1,22 +1,12 @@ package gt.app.modules.user; -import gt.api.email.EmailDto; -import gt.app.api.EmailClient; -import gt.app.config.Constants; -import gt.app.config.security.AppUserDetails; import gt.app.domain.AppUser; -import gt.app.domain.LiteUser; -import gt.app.exception.RecordNotFoundException; -import gt.app.modules.user.dto.PasswordUpdateDTO; -import gt.app.modules.user.dto.UserProfileUpdateDTO; -import gt.app.modules.user.dto.UserSignUpDTO; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Set; +import java.util.UUID; @Service @Transactional @@ -25,61 +15,20 @@ public class UserService { private final UserRepository userRepository; - private final PasswordEncoder passwordEncoder; - private final AuthorityService authorityService; - private final EmailClient emailService; - - private final LiteUserRepository liteUserRepository; - - public void update(UserProfileUpdateDTO toUpdate, AppUserDetails userDetails) { - LiteUser user = liteUserRepository.findOneByUsername(userDetails.getUsername()) - .orElseThrow(() -> new RecordNotFoundException("User", "login", userDetails.getUsername())); - - user.setFirstName(toUpdate.getFirstName()); - user.setLastName(toUpdate.getLastName()); - user.setEmail(toUpdate.getEmail()); - - liteUserRepository.save(user); - } - - public void updatePassword(PasswordUpdateDTO toUpdate, AppUserDetails userDetails) { - LiteUser user = liteUserRepository.findOneByUsername(userDetails.getUsername()) - .orElseThrow(() -> new RecordNotFoundException("User", "login", userDetails.getUsername())); - - user.setPassword(passwordEncoder.encode(toUpdate.pwdPlainText())); - liteUserRepository.save(user); - } - - public AppUser create(UserSignUpDTO toCreate) { - - //validation is already done - - var user = new AppUser(toCreate.getUsername(), toCreate.getFirstName(), toCreate.getLastName(), toCreate.getEmail()); - - user.setPassword(passwordEncoder.encode(toCreate.getPwdPlaintext())); - - user.setAuthorities(authorityService.findByNameIn(Constants.ROLE_USER)); - - userRepository.save(user); - - EmailDto dto = EmailDto.of("system@noteapp", Set.of(user.getEmail()), - "NoteApp Account Created!", - "Thanks for signing up."); - - emailService.sendEmailWithAttachments(dto); - - return user; - } public AppUser save(AppUser u) { return userRepository.save(u); } + public boolean exists(UUID id) { + return userRepository.existsById(id); + } + public boolean existsByUsername(String username) { return userRepository.existsByUsername(username); } - public AppUser getReference(Long id) { + public AppUser getReference(UUID id) { return userRepository.getReferenceById(id); } diff --git a/main-app/main-webapp/src/main/java/gt/app/web/mvc/AccountController.java b/main-app/main-webapp/src/main/java/gt/app/web/mvc/AccountController.java index 79eace3..e54f94c 100644 --- a/main-app/main-webapp/src/main/java/gt/app/web/mvc/AccountController.java +++ b/main-app/main-webapp/src/main/java/gt/app/web/mvc/AccountController.java @@ -1,10 +1,10 @@ package gt.app.web.mvc; -import gt.app.config.security.AppUserDetails; +import gt.app.config.security.CurrentUser; +import gt.app.config.security.CurrentUserToken; import gt.app.modules.user.UserService; import gt.app.modules.user.UserStat; import lombok.RequiredArgsConstructor; -import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -18,7 +18,7 @@ class AccountController { final UserService userService; @GetMapping("/account/user/{id}") - public String getUserSummary(Model model, @AuthenticationPrincipal AppUserDetails loggedInUserDtl, @PathVariable UUID id) { + public String getUserSummary(Model model, @CurrentUser CurrentUserToken loggedInUserDtl, @PathVariable UUID id) { model.addAttribute("name", "FName LastName"); diff --git a/main-app/main-webapp/src/main/java/gt/app/web/mvc/ArticleController.java b/main-app/main-webapp/src/main/java/gt/app/web/mvc/ArticleController.java index e948b21..9fe44ca 100644 --- a/main-app/main-webapp/src/main/java/gt/app/web/mvc/ArticleController.java +++ b/main-app/main-webapp/src/main/java/gt/app/web/mvc/ArticleController.java @@ -1,6 +1,7 @@ package gt.app.web.mvc; -import gt.app.config.security.AppUserDetails; +import gt.app.config.security.CurrentUser; +import gt.app.config.security.CurrentUserToken; import gt.app.domain.Article; import gt.app.modules.article.*; import gt.app.utl.PaginationUtil; @@ -10,7 +11,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -29,15 +29,15 @@ class ArticleController { final CommentService commentService; @GetMapping({"/", ""}) - public String userHome(Model model, @AuthenticationPrincipal AppUserDetails u, Pageable pageable) { - var page = articleService.previewAllWithFilesByUser(PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by("createdDate").descending()), u.getId()); + public String userHome(Model model, @CurrentUser CurrentUserToken u, Pageable pageable) { + var page = articleService.previewAllWithFilesByUser(PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by("createdDate").descending()), u.getUserId()); model.addAttribute("articles", page); PaginationUtil.decorateModel(model, page); return "article"; } @GetMapping("/new") - public String startNewArticle(Model model, @AuthenticationPrincipal AppUserDetails loggedInUserDtl) { + public String startNewArticle(Model model, @CurrentUser CurrentUserToken loggedInUserDtl) { model.addAttribute("article", new Article()); //new article box at top return "article/new-article"; } diff --git a/main-app/main-webapp/src/main/java/gt/app/web/mvc/AuthController.java b/main-app/main-webapp/src/main/java/gt/app/web/mvc/AuthController.java new file mode 100644 index 0000000..0609a52 --- /dev/null +++ b/main-app/main-webapp/src/main/java/gt/app/web/mvc/AuthController.java @@ -0,0 +1,57 @@ +package gt.app.web.mvc; + +import gt.app.config.AppProperties; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.stereotype.Controller; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequestMapping("/auth") +public class AuthController { + + private final ClientRegistration registration; + private final AppProperties appProperties; + + public AuthController(ClientRegistrationRepository registrations, AppProperties appProperties) { + this.registration = registrations.findByRegistrationId("oidc"); + this.appProperties = appProperties; + } + + //FIXME: use keycloak rest api to handle these + +// @GetMapping("/change-password") +// public String changePwd(RedirectAttributes redirectAttributes, HttpServletRequest req, HttpServletResponse resp) { +// return getKeyCloakAccountUrl(redirectAttributes, req, resp) + "/#/security/signingin"; +// } +// +// @GetMapping("/settings") +// public String settings(RedirectAttributes redirectAttributes, HttpServletRequest req, HttpServletResponse resp) { +// return getKeyCloakAccountUrl(redirectAttributes, req, resp); +// } + + @GetMapping("/logout") + public String logout(HttpServletRequest request, @AuthenticationPrincipal(expression = "idToken") OidcIdToken idToken) { + var logoutUrlSb = new StringBuilder(); + logoutUrlSb.append(this.registration.getProviderDetails().getConfigurationMetadata().get("end_session_endpoint").toString()); + String originUrl = request.getHeader(HttpHeaders.ORIGIN); + if (!StringUtils.hasText(originUrl)) { + originUrl = appProperties.getWeb().getBaseUrl() + "?logout=true"; + } + logoutUrlSb.append("?id_token_hint=").append(idToken.getTokenValue()).append("&post_logout_redirect_uri=").append(originUrl); + + request.getSession().invalidate(); + return "redirect:" + logoutUrlSb; + } + + @GetMapping("/login") + public String login() { + return "redirect:/oauth2/authorization/oidc"; //this will redirect to login page + } +} diff --git a/main-app/main-webapp/src/main/java/gt/app/web/mvc/ErrorControllerAdvice.java b/main-app/main-webapp/src/main/java/gt/app/web/mvc/ErrorControllerAdvice.java index d029a44..2ec517c 100644 --- a/main-app/main-webapp/src/main/java/gt/app/web/mvc/ErrorControllerAdvice.java +++ b/main-app/main-webapp/src/main/java/gt/app/web/mvc/ErrorControllerAdvice.java @@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.servlet.NoHandlerFoundException; @ControllerAdvice @Slf4j @@ -16,7 +17,9 @@ class ErrorControllerAdvice { @ExceptionHandler(Throwable.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public String exception(final Throwable throwable, final Model model) { - log.error("Exception during execution of application", throwable); + if(!(throwable instanceof NoHandlerFoundException)){ + log.error("Exception during execution of application", throwable); + } String errorMessage = (throwable != null ? throwable.getMessage() : "Unknown error"); model.addAttribute("errorMessage", errorMessage); diff --git a/main-app/main-webapp/src/main/java/gt/app/web/mvc/IndexController.java b/main-app/main-webapp/src/main/java/gt/app/web/mvc/IndexController.java index c07e76d..422b713 100644 --- a/main-app/main-webapp/src/main/java/gt/app/web/mvc/IndexController.java +++ b/main-app/main-webapp/src/main/java/gt/app/web/mvc/IndexController.java @@ -1,5 +1,6 @@ package gt.app.web.mvc; +import gt.app.api.ReportClient; import gt.app.domain.Article; import gt.app.modules.article.ArticleService; import gt.app.utl.PaginationUtil; @@ -18,6 +19,7 @@ class IndexController { private final ArticleService articleService; + private final ReportClient reportClient; @GetMapping({"/", ""}) public String index(Model model, Pageable pageable) { @@ -32,6 +34,8 @@ public String index(Model model, Pageable pageable) { @GetMapping("/admin") public String adminHome(Model model, Pageable pageable) { var page = articleService.getAllToReview(PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by("createdDate").descending())); + var count = reportClient.getFlaggedForReviewCount(); + model.addAttribute("totalToReview", count.value()); model.addAttribute("articlesToReview", page); PaginationUtil.decorateModel(model, page); return "admin/admin-area"; diff --git a/main-app/main-webapp/src/main/java/gt/app/web/mvc/UserController.java b/main-app/main-webapp/src/main/java/gt/app/web/mvc/UserController.java deleted file mode 100644 index b4ee4c2..0000000 --- a/main-app/main-webapp/src/main/java/gt/app/web/mvc/UserController.java +++ /dev/null @@ -1,98 +0,0 @@ -package gt.app.web.mvc; - -import gt.app.config.security.AppUserDetails; -import gt.app.modules.user.PasswordUpdateValidator; -import gt.app.modules.user.UserService; -import gt.app.modules.user.UserSignupValidator; -import gt.app.modules.user.dto.PasswordUpdateDTO; -import gt.app.modules.user.dto.UserProfileUpdateDTO; -import gt.app.modules.user.dto.UserSignUpDTO; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.validation.BindingResult; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; - -@Controller -@RequiredArgsConstructor -class UserController { - - private final UserService userService; - private final UserSignupValidator userSignupValidator; - private final PasswordUpdateValidator passwordUpdateValidator; - - @GetMapping(value = "/signup") - public String register(Model model) { - model.addAttribute("user", new UserSignUpDTO()); - return "user/signup"; - } - - @PostMapping(value = "/signup") - public String register(@Valid @ModelAttribute("user") UserSignUpDTO user, BindingResult bindingResult, - RedirectAttributes redirectAttrs) { - - //do custom validation(MVC style) along with the BeanValidation - userSignupValidator.validate(user, bindingResult); - - if (bindingResult.hasErrors()) { - return "user/signup"; - } - - userService.create(user); - - redirectAttrs.addFlashAttribute("success", "User " + user.getUsername() + " is created"); - - return "redirect:/"; - } - - @GetMapping(value = "/profile") - public String updateProfile(Model model, @AuthenticationPrincipal AppUserDetails loggedInUserDtl) { - model.addAttribute("user", new UserProfileUpdateDTO(loggedInUserDtl.getEmail(), loggedInUserDtl.getFirstName(), loggedInUserDtl.getLastName())); - return "user/profile"; - } - - @PostMapping(value = "/profile") - public String updateProfile(@Valid @ModelAttribute("user") UserProfileUpdateDTO user, BindingResult bindingResult, - @AuthenticationPrincipal AppUserDetails loggedInUserDtl, RedirectAttributes redirectAttrs) { - - if (bindingResult.hasErrors()) { - return "user/profile"; - } - - userService.update(user, loggedInUserDtl); - - redirectAttrs.addFlashAttribute("success", "Profile updated successfully"); - - return "redirect:/"; //self - } - - @GetMapping(value = "/password") - public String updatePassword(Model model) { - model.addAttribute("user", PasswordUpdateDTO.of()); - return "user/password"; - } - - @PostMapping(value = "/password") - public String updatePassword(@Valid @ModelAttribute("user") PasswordUpdateDTO reqDto, BindingResult bindingResult, - @AuthenticationPrincipal AppUserDetails loggedInUserDtl, RedirectAttributes redirectAttrs) { - - //do custom validation along with the BeanValidation - passwordUpdateValidator.validate(reqDto, bindingResult, loggedInUserDtl); - - if (bindingResult.hasErrors()) { - return "user/password"; - } - - userService.updatePassword(reqDto, loggedInUserDtl); - - redirectAttrs.addFlashAttribute("success", "Password updated successfully"); - - return "redirect:/"; - } - -} diff --git a/main-app/main-webapp/src/main/java/gt/app/web/rest/UserResource.java b/main-app/main-webapp/src/main/java/gt/app/web/rest/UserResource.java index 64bf0f8..4df18a9 100644 --- a/main-app/main-webapp/src/main/java/gt/app/web/rest/UserResource.java +++ b/main-app/main-webapp/src/main/java/gt/app/web/rest/UserResource.java @@ -1,5 +1,6 @@ package gt.app.web.rest; +import gt.app.config.security.CurrentUserToken; import gt.app.config.security.SecurityUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -7,8 +8,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.Optional; - @RestController @RequestMapping("/api") @RequiredArgsConstructor @@ -16,7 +15,11 @@ class UserResource { @GetMapping("/account") - public Optional getAccount() { - return SecurityUtils.getCurrentUserId(); + public CurrentUserToken.UserToken getAccount() { + var user = SecurityUtils.getCurrentUserDetails(); + if (user != null) { + user.getUserToken(); + } + return null; } } diff --git a/main-app/main-webapp/src/main/resources/application-dev.yml b/main-app/main-webapp/src/main/resources/application-dev.yml index 00df16f..e6d1045 100644 --- a/main-app/main-webapp/src/main/resources/application-dev.yml +++ b/main-app/main-webapp/src/main/resources/application-dev.yml @@ -41,6 +41,8 @@ spring: feign-clients: email-service: url: http://localhost:8085/ #TODO: use service discovery + report-service: + url: http://localhost:8086/ #TODO: use service discovery logging.level: org.jooq.tools.LoggerListener: DEBUG diff --git a/main-app/main-webapp/src/main/resources/application.yml b/main-app/main-webapp/src/main/resources/application.yml index cb36021..463b274 100644 --- a/main-app/main-webapp/src/main/resources/application.yml +++ b/main-app/main-webapp/src/main/resources/application.yml @@ -20,7 +20,21 @@ spring: openfeign: micrometer: enabled: true - + security: + oauth2: + client: + # we are using authorization code flow (login within keycloak app web ui) + provider: + oidc: + # use http://localhost:8082/realms/articleapp/.well-known/openid-configuration to get config from keycloak + issuer-uri: http://localhost:${KEYCLOAK_PORT:8082}/realms/seedapp + registration: + oidc: + # authorization-grant-type: refresh_token + client-id: seed-app-gateway-client + client-secret: secret123 + scope: openid, profile, email, offline_access # last one for refresh tokens + # we can have multiple auth providers server: port: 8081 @@ -41,7 +55,8 @@ app-properties: content-checkercallback-response-queue: jms-content-checkercallback-responsequeue file-storage: upload-folder: ${java.io.tmpdir}/uploads - + web: + base-url: http://localhost:${server.port} wro4j: jmx-enabled: false debug: true diff --git a/main-app/main-webapp/src/main/resources/keycloak/realm-export.json b/main-app/main-webapp/src/main/resources/keycloak/realm-export.json new file mode 100644 index 0000000..486cbdf --- /dev/null +++ b/main-app/main-webapp/src/main/resources/keycloak/realm-export.json @@ -0,0 +1,2585 @@ +{ + "id": "seed-app", + "realm": "seedapp", + "displayName": "See App IAM", + "notBefore": 1580089862, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "8e986fb5-dafb-43bf-a7c2-7e57572d3d80", + "name": "ROLE_ADMIN", + "description": "seedapp administrator role", + "composite": false, + "clientRole": false, + "containerId": "seed-app", + "attributes": {} + }, + { + "id": "e1b19afd-f612-4a79-bdf8-26a99b89b10b", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "seed-app", + "attributes": {} + }, + { + "id": "ec5705e1-fc1d-4d21-8364-abd3bd4efcd0", + "name": "ROLE_USER", + "description": "seedapp user role", + "composite": false, + "clientRole": false, + "containerId": "seed-app", + "attributes": {} + }, + { + "id": "d2b73e7b-a2d7-40e9-8ebc-2af00454e8aa", + "name": "default-roles-seedapp", + "description": "${role_default-roles}", + "composite": true, + "composites": { + "realm": [ + "offline_access", + "uma_authorization" + ], + "client": { + "account": [ + "view-profile", + "manage-account" + ] + } + }, + "clientRole": false, + "containerId": "seed-app", + "attributes": {} + }, + { + "id": "2eec61d0-9581-4dbf-8c7b-f32dc5fac3ce", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "seed-app", + "attributes": {} + } + ], + "client": { + "internal": [], + "realm-management": [ + { + "id": "a6249a12-d76c-4514-b137-e3018b243e25", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "898488c8-e260-41c5-a463-7ceea14d587a", + "attributes": {} + }, + { + "id": "a28bc401-c5ad-4fab-aef4-42629988c10b", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "898488c8-e260-41c5-a463-7ceea14d587a", + "attributes": {} + }, + { + "id": "464bca1f-136f-45de-a7fc-b976a185ce7e", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-users", + "query-groups" + ] + } + }, + "clientRole": true, + "containerId": "898488c8-e260-41c5-a463-7ceea14d587a", + "attributes": {} + }, + { + "id": "98c2fa77-d3c8-4f68-b9f4-b79f87efd4a9", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "898488c8-e260-41c5-a463-7ceea14d587a", + "attributes": {} + }, + { + "id": "6b82bfdb-c8de-4274-95b4-a683eb4ead50", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "898488c8-e260-41c5-a463-7ceea14d587a", + "attributes": {} + }, + { + "id": "3c6b9cfe-80c4-41d5-a5ac-0cadebacfc8d", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "898488c8-e260-41c5-a463-7ceea14d587a", + "attributes": {} + }, + { + "id": "23676fb8-235a-4e54-a0d0-9bed1ccbe2f8", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "898488c8-e260-41c5-a463-7ceea14d587a", + "attributes": {} + }, + { + "id": "b71fe952-bb06-4e4a-91ef-2d2714f770e1", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "898488c8-e260-41c5-a463-7ceea14d587a", + "attributes": {} + }, + { + "id": "0813cbd0-c73d-469d-a54d-84a865c302af", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "898488c8-e260-41c5-a463-7ceea14d587a", + "attributes": {} + }, + { + "id": "c7a4f4c1-9089-458c-a765-f6d22ea94690", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "898488c8-e260-41c5-a463-7ceea14d587a", + "attributes": {} + }, + { + "id": "2e1bc884-e9d3-45d2-909c-2777a78ca8ae", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "898488c8-e260-41c5-a463-7ceea14d587a", + "attributes": {} + }, + { + "id": "0a05451e-7d64-4e87-b585-f1143ce5752e", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "898488c8-e260-41c5-a463-7ceea14d587a", + "attributes": {} + }, + { + "id": "dfad4d08-6d75-42b6-8699-4886e47bc464", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "898488c8-e260-41c5-a463-7ceea14d587a", + "attributes": {} + }, + { + "id": "392ed0a3-f6ad-48a1-b201-648037d2b4bd", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "manage-authorization", + "view-realm", + "view-users", + "query-users", + "manage-identity-providers", + "view-identity-providers", + "query-groups", + "impersonation", + "manage-events", + "query-clients", + "manage-realm", + "view-authorization", + "view-events", + "view-clients", + "create-client", + "manage-clients", + "manage-users", + "query-realms" + ] + } + }, + "clientRole": true, + "containerId": "898488c8-e260-41c5-a463-7ceea14d587a", + "attributes": {} + }, + { + "id": "d7efdf61-affb-42a1-bcb0-b2c30d87a39e", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "898488c8-e260-41c5-a463-7ceea14d587a", + "attributes": {} + }, + { + "id": "14da8e56-5c8b-4764-96da-250449a32fd4", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "898488c8-e260-41c5-a463-7ceea14d587a", + "attributes": {} + }, + { + "id": "88e6a9f5-259c-487d-af35-2a98da066816", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "898488c8-e260-41c5-a463-7ceea14d587a", + "attributes": {} + }, + { + "id": "932273a7-c02b-43db-81c5-96a0dc45e454", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "898488c8-e260-41c5-a463-7ceea14d587a", + "attributes": {} + }, + { + "id": "e3edf335-cec5-4012-a00d-fcac045052e1", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "898488c8-e260-41c5-a463-7ceea14d587a", + "attributes": {} + } + ], + "seedapp-control-center": [], + "security-admin-console": [], + "seed-app-gateway-client": [], + "admin-cli": [], + "account-console": [], + "broker": [ + { + "id": "5b08a930-9f1d-4030-ae75-92c1e4c9352c", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "88e1225b-f0b9-46ba-8efd-f2c10ce23058", + "attributes": {} + } + ], + "account": [ + { + "id": "a88c56b8-6bc9-418a-92bc-7a17e7707f60", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "6cc5a716-0880-47dc-b714-9a4967246b2f", + "attributes": {} + }, + { + "id": "0cb954ab-987f-482a-b2d7-0d481ba1d532", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "6cc5a716-0880-47dc-b714-9a4967246b2f", + "attributes": {} + }, + { + "id": "6450156d-7526-48f2-8ea0-bb1e51f9eefa", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } + }, + "clientRole": true, + "containerId": "6cc5a716-0880-47dc-b714-9a4967246b2f", + "attributes": {} + }, + { + "id": "e5b2ba76-4c36-4ba1-b210-89a1ac3c6bbe", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "6cc5a716-0880-47dc-b714-9a4967246b2f", + "attributes": {} + }, + { + "id": "35537940-67a6-4217-881b-1ff98109b374", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": [ + "view-consent" + ] + } + }, + "clientRole": true, + "containerId": "6cc5a716-0880-47dc-b714-9a4967246b2f", + "attributes": {} + }, + { + "id": "5ebf404b-7805-4da2-abb4-9db7d3b36120", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "6cc5a716-0880-47dc-b714-9a4967246b2f", + "attributes": {} + }, + { + "id": "a7f45fab-19c3-4c48-aca3-85f828ca0fed", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "6cc5a716-0880-47dc-b714-9a4967246b2f", + "attributes": {} + } + ] + } + }, + "groups": [ + { + "id": "afb0c768-ab0f-454c-a8ea-bc9e70b50248", + "name": "Admins", + "path": "/Admins", + "attributes": {}, + "realmRoles": [ + "ROLE_ADMIN" + ], + "clientRoles": {}, + "subGroups": [] + }, + { + "id": "672767bb-4ab0-4d37-93a1-9b6c2416b6b2", + "name": "Users", + "path": "/Users", + "attributes": {}, + "realmRoles": [ + "ROLE_USER" + ], + "clientRoles": {}, + "subGroups": [] + } + ], + "defaultRole": { + "id": "d2b73e7b-a2d7-40e9-8ebc-2af00454e8aa", + "name": "default-roles-seedapp", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "seed-app" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpSupportedApplications": [ + "FreeOTP", + "Google Authenticator" + ], + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "users": [ + { + "id": "eeefadeb-340e-4416-ab0f-68a9da67a0e9", + "createdTimestamp": 1580092657614, + "username": "gtiwari", + "enabled": true, + "totp": false, + "emailVerified": true, + "firstName": "Ganesh", + "lastName": "Tiwari", + "email": "gtiwari@email", + "credentials": [ + { + "id": "4d636339-31c2-47a7-8024-c1e6fdb08462", + "type": "password", + "createdDate": 1580092657696, + "secretData": "{\"value\":\"vgLtDly2/bzpi0DMGn33wrTQrGV2KBV2e2GTiwhaUIT+BJVQ5XwoLRJbFU5ZaJXBmzdLv1/iO/NLmSeuYmiOQQ==\",\"salt\":\"GMaMEZT1aXSLCY1JiA7ivw==\"}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\"}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "offline_access", + "uma_authorization" + ], + "clientRoles": { + "account": [ + "view-profile", + "manage-account" + ] + }, + "notBefore": 0, + "groups": [ + "/Admins", + "/Users" + ] + }, + { + "id": "a621ac4c-6172-4103-9050-b27c053b11eb", + "createdTimestamp": 1580005071465, + "username": "system", + "email": "system@email", + "enabled": true, + "totp": false, + "emailVerified": true, + "credentials": [ + { + "id": "85153370-dac3-4c4e-ad0f-d56be0989ac1", + "type": "password", + "createdDate": 1580005084934, + "secretData": "{\"value\":\"UAFe+kqnvwEM0ZXifVch3M/i6e5baljh/D7tgX9+UqlLywqf5ANulu7e6Qy/qYs/6nYFy/DxE88H+BWUlgHS3A==\",\"salt\":\"L6BvFcInxqsOBvS/itv6pQ==\"}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\"}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "offline_access", + "uma_authorization" + ], + "clientRoles": { + "account": [ + "view-profile", + "manage-account" + ] + }, + "notBefore": 0, + "groups": [ + "/Admins", + "/Users" + ] + }, + { + "id": "d1460f56-7f7e-43e1-8396-bddf39dba08f", + "createdTimestamp": 1580005095668, + "username": "user1", + "email": "user1@email", + "enabled": true, + "totp": false, + "emailVerified": true, + "firstName": "User 1", + "lastName": "Tiwari", + "credentials": [ + { + "id": "e9ee782e-ba45-4b26-94e0-0a9125dcd3c5", + "type": "password", + "createdDate": 1580005125789, + "secretData": "{\"value\":\"SJONVU1ktBTDrsDZ8U15g/gYsYvLFVy7IcVFd4IrDtEwau9DqznYdJZp+wOqzTI3VF4YQV2Mih/pVciljgTnHQ==\",\"salt\":\"jxoPcDGtujZ0cO44I75mgA==\"}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\"}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "offline_access", + "uma_authorization" + ], + "clientRoles": { + "account": [ + "view-profile", + "manage-account" + ] + }, + "notBefore": 0, + "groups": [ + "/Users" + ] + }, + { + "id": "fa6820a5-cf39-4cbf-9e50-89cc832bebee", + "createdTimestamp": 1580005143120, + "username": "user2", + "email": "user2@email", + "enabled": true, + "totp": false, + "emailVerified": true, + "firstName": "User 2", + "lastName": "Tiwari", + "credentials": [ + { + "id": "20f32b3e-1ee2-49f2-8484-0d7dcb555ea8", + "type": "password", + "createdDate": 1580005153215, + "secretData": "{\"value\":\"9j7daptNaNi46sh5GVnKbFesTVvrdYjzYFmS1CM5+TlvElOPeeQjHNMou/a//rTm527taKAkaqxPzmAMd98/SA==\",\"salt\":\"sj7WlqvcbmItkmpeNRDfBA==\"}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\"}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "offline_access", + "uma_authorization" + ], + "clientRoles": { + "account": [ + "view-profile", + "manage-account" + ] + }, + "notBefore": 0, + "groups": [ + "/Users" + ] + } + ], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account" + ] + } + ] + }, + "clients": [ + { + "id": "6cc5a716-0880-47dc-b714-9a4967246b2f", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/seedapp/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [ + "/realms/seedapp/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "fb0a4870-06db-4f9d-9d44-baf51a00cc34", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/seedapp/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [ + "/realms/seedapp/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "c5c4ebe5-d009-4f96-b143-1b36d770eafb", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "bb166356-838d-445e-94e3-9330ad7ab51b", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "88e1225b-f0b9-46ba-8efd-f2c10ce23058", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "10e6ea34-9f1e-49ef-8e28-7eb851459694", + "clientId": "internal", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "internal", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": true, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "saml.assertion.signature": "false", + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "saml.encrypt": "false", + "post.logout.redirect.uris": "+", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "exclude.session.state.from.auth.response": "false", + "saml_force_name_id_format": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "ff2f50b4-5409-4789-bdda-fe731f14fbff", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + }, + { + "id": "72f9ae74-9e95-4b7b-a709-5086137410bb", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientId", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientId", + "jsonType.label": "String" + } + }, + { + "id": "029bf6c8-5a19-4798-984c-bdb205d752d5", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "1acf7ad0-68cb-46a6-a3e4-8b2f2abecb85", + "clientId": "seedapp-control-center", + "rootUrl": "http://localhost:7419", + "adminUrl": "http://localhost:7419", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "seedapp-control-center", + "redirectUris": [ + "dev.localhost.ionic:*", + "http://127.0.0.1:*", + "http://localhost:*", + "https://127.0.0.1:*", + "https://localhost:*" + ], + "webOrigins": [ + "*" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": true, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "saml.assertion.signature": "false", + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "saml.encrypt": "false", + "post.logout.redirect.uris": "+", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "exclude.session.state.from.auth.response": "false", + "saml_force_name_id_format": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "seedapp", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "898488c8-e260-41c5-a463-7ceea14d587a", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "989d2b96-b820-4f9b-aa17-55e6488b08c8", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/seedapp/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [ + "/admin/seedapp/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "5fd34289-c644-411a-874c-849475d9d102", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "6e8deddb-b4d6-4e2e-b389-b397d3f74fcd", + "clientId": "seed-app-gateway-client", + "rootUrl": "http://localhost:8081", + "adminUrl": "http://localhost:8081", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "secret123", + "redirectUris": [ + "dev.localhost.ionic:*", + "http://127.0.0.1:*", + "http://localhost:*", + "https://127.0.0.1:*", + "https://localhost:*", + "https://oauth.pstmn.io/v1/callback" + ], + "webOrigins": [ + "*" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": true, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "saml.assertion.signature": "false", + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "saml.encrypt": "false", + "post.logout.redirect.uris": "+", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "exclude.session.state.from.auth.response": "false", + "saml_force_name_id_format": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "seedapp", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "52d73c82-423c-44a8-b2ec-1e13f4cd6065", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "98230752-36b9-4755-8661-a7de1926d0d4", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "44d24405-87bf-4b37-a627-e3fdabb93f50", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "36800088-6d17-4c18-93e8-2bc93901d8b7", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "3ea34afd-30b5-4e5d-a836-dbda439dce6f", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "9816de82-24b7-42fe-a85a-1264868ec293", + "name": "seedapp", + "description": "seedapp specific claims", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "0f9c9347-aad6-4bff-94f4-e11937f2ad33", + "name": "langKey", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "langKey", + "id.token.claim": "false", + "access.token.claim": "false", + "claim.name": "langKey", + "jsonType.label": "String" + } + }, + { + "id": "69729907-8d1c-4961-81c0-91766f548cc9", + "name": "roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "userinfo.token.claim": "true", + "id.token.claim": "false", + "access.token.claim": "true", + "claim.name": "roles", + "jsonType.label": "String" + } + }, + { + "id": "336acfe2-a717-492a-9055-5b70e808f42f", + "name": "login", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "preferred_username", + "id.token.claim": "false", + "access.token.claim": "false", + "claim.name": "login", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "87d299f2-434f-4abd-8cb0-a16231acd713", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "fce09d51-cb85-4ccd-b83d-865a4d4bf650", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "userinfo.token.claim": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + }, + { + "id": "3d1ee7e2-b7e1-4504-bd52-b47a2cb10eec", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "0399b625-22d7-4d68-b4db-fd1dc2effacc", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "2b867b2d-3373-43ff-b50f-ea37a5e1c390", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "daa0191b-20d1-4f71-b191-6c48a37e3677", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + }, + { + "id": "32213de7-12f7-4864-b696-c8e6c5e0c26e", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "60a44832-9776-449f-94cd-fa8c24a75f35", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "a59584ab-7a7c-4b23-95b5-be8dbbfadc6f", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "d382c1dc-d5d8-479e-8809-f0a618113a07", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "559f86c1-1187-498d-8354-723f4ea5721c", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "0925e106-a8e2-4ad1-b75e-4147d185894a", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "String" + } + }, + { + "id": "eb8e2c73-5c65-4b53-8d55-46edef61315b", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "4c109376-01bc-4b69-a3c0-4b830ecad674", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "b3813956-e556-4b57-a06b-f71b0d6f3d47", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "28beb4c0-029b-4aa5-ad5f-6d824ca67e15", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "53d681bc-ec29-4f57-924b-ff5bd22d4093", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "12ba8e12-157d-4729-918b-0d74fa444fba", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "ddb818fe-8e4a-4b26-9c5d-2467a26af6dc", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "f78b1746-2be1-45f4-9c1e-1f6141ccdb65", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "7723245c-4952-4822-86ae-084048b1f2f2", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "b192fe9f-aa82-4d7d-b8c7-eb7d1ba888d4", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "d181691e-b4a6-4063-9eba-6b984402a9a7", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "724b16d4-8a9b-42d8-850f-99ca1ab3c958", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "915fcb95-81da-4e4c-86ee-73f3b52c83e9", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "12f0b32d-8911-4028-809b-fc1c0e5e9207", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + }, + { + "id": "5b997b66-937f-46d3-9e8b-70dca949f682", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "cdcd6969-a9aa-4de5-adbe-dc83da4184c5", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + }, + { + "id": "2daaac74-636f-4074-87a9-d1aba9dffb96", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "752e035f-038d-46ac-b65d-91f863fdd986", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + } + ], + "defaultDefaultClientScopes": [ + "web-origins", + "email", + "profile", + "roles", + "role_list" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "phone", + "address", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "827fde01-dc1b-4c1f-a529-9ef833ca3432", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-property-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-user-attribute-mapper", + "saml-user-property-mapper", + "oidc-address-mapper", + "oidc-full-name-mapper", + "saml-role-list-mapper", + "oidc-usermodel-attribute-mapper" + ] + } + }, + { + "id": "0a429e7e-be7a-46b4-b42a-d1f8b265ff16", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "5a1ff0b4-250f-48ee-8169-abff30cf7534", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "c79f6629-84a9-467c-81d0-63e20b19f916", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "b6b23ef8-96e8-4e2e-8efe-8003057a8d42", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "id": "36dfaa02-0252-4448-9cdf-a17abf239f78", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "8216421d-34fb-4726-8331-137217657bdb", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-address-mapper", + "oidc-usermodel-attribute-mapper", + "saml-user-property-mapper", + "saml-user-attribute-mapper", + "saml-role-list-mapper", + "oidc-full-name-mapper", + "oidc-usermodel-property-mapper", + "oidc-sha256-pairwise-sub-mapper" + ] + } + }, + { + "id": "d045f3f9-15e6-4e69-a419-0e7ff8a635ef", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + } + ], + "org.keycloak.userprofile.UserProfileProvider": [ + { + "id": "b05ccf0d-d8ac-4695-bd60-37018f8f94b4", + "providerId": "declarative-user-profile", + "subComponents": {}, + "config": {} + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "62707fae-58f9-4fc2-89fb-0c5d212dc3dc", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "4a8480bc-96fd-4906-a907-f948a73bab38", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "HS256" + ] + } + }, + { + "id": "40c01a32-0c0b-4dbb-9595-e5a5c8d26bc4", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "491fbbc9-b70b-45bd-8243-2039ae3f115d", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "2c63ad60-76ab-4350-9def-74328bab70d0", + "alias": "Authentication Options", + "description": "Authentication options.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "basic-auth", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "basic-auth-otp", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "82b9b584-2243-4893-b58c-4567f34434a6", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "e70e7c74-8ab5-411c-b06c-d478a452bee3", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "4f3e6fdd-9b4d-4dc0-946a-1e1ccae7af71", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "aa66c794-f21b-4663-9de1-9e27a7e425ab", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "Account verification options", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "2d4499a0-399c-4b6c-970c-7b441498f7b9", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "710f4172-56a5-466e-bc75-ad7405ff62b5", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "da7d3a39-7077-4354-9ffc-5b9f79fbaf0d", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "6285968e-6200-463a-a329-8c60bc8fe9fc", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "flowAlias": "forms", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "10393f04-3922-40db-a622-2655dfcae45d", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "4e5e164e-3c7e-4ca5-a10c-d7b817a7d468", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "80f88b0b-70de-4e4c-ae56-0293558301c5", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "821af41a-6e77-4e8c-85a6-0280d5268909", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "User creation or linking", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "c4058fb0-ad93-4595-96ef-7d4bc5cbef4d", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "a2a1d056-2521-498f-b345-b7db56f9342c", + "alias": "http challenge", + "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "no-cookie-redirect", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "Authentication Options", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "13e68e1b-4b44-4f21-a253-5b2dea24404b", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "flowAlias": "registration form", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "e7588789-22d4-459b-96d6-1b480520f487", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-profile-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "8dc399ef-cf7d-46d5-9688-678c146ea8c4", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "47ab5a7a-f67a-4a66-bdac-932ee230000d", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "b12be521-4e2b-42f0-a1a2-f1ba47ab4854", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "58bf2d56-1c45-4acc-9005-23b978d961d7", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "terms_and_conditions", + "name": "Terms and Conditions", + "providerId": "terms_and_conditions", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DeviceCodeLifespan": "600", + "clientOfflineSessionMaxLifespan": "0", + "oauth2DevicePollingInterval": "5", + "clientSessionIdleTimeout": "0", + "clientSessionMaxLifespan": "0", + "parRequestUriLifespan": "60", + "clientOfflineSessionIdleTimeout": "0", + "cibaInterval": "5", + "frontendUrl": "", + "acr.loa.map": "[]" + }, + "keycloakVersion": "19.0.1", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } +} diff --git a/main-app/main-webapp/src/main/resources/templates/_fragments/header.html b/main-app/main-webapp/src/main/resources/templates/_fragments/header.html index 38456ed..550d6c2 100644 --- a/main-app/main-webapp/src/main/resources/templates/_fragments/header.html +++ b/main-app/main-webapp/src/main/resources/templates/_fragments/header.html @@ -44,7 +44,7 @@ @@ -71,7 +71,7 @@