spring security for keycloak roles

This commit is contained in:
Jörg Henke
2023-08-04 19:16:18 +02:00
parent e7058a8b02
commit 485bc5e8c3
15 changed files with 98 additions and 43 deletions

View File

@ -7,7 +7,7 @@ plugins {
apply plugin: 'io.spring.dependency-management'
group = 'de.jottyfan'
version = '1.2.9'
version = '1.3.0'
description = """timetrack"""

View File

@ -0,0 +1,78 @@
package de.jottyfan.timetrack.config;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
/**
*
* @author jotty
*
*/
@Configuration
public class AuthorizationConfiguration {
private static final String REALM_ACCESS_CLAIM = "realm_access";
private static final String ROLES_CLAIM = "roles";
private static final String RESOURCE_ACCESS_CLAIM = "resource_access";
@Value("${spring.security.oauth2.client.registration.keycloak.client-id}")
private String clientId;
@Bean
GrantedAuthoritiesMapper userAuthoritiesMapperForKeycloak() {
return authorities -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
var authority = authorities.iterator().next();
boolean isOidc = authority instanceof OidcUserAuthority;
if (isOidc) {
var oidcUserAuthority = (OidcUserAuthority) authority;
var userInfo = oidcUserAuthority.getUserInfo();
if (userInfo.hasClaim(REALM_ACCESS_CLAIM)) {
var realmAccess = userInfo.getClaimAsMap(REALM_ACCESS_CLAIM);
@SuppressWarnings("unchecked")
var roles = (Collection<String>) realmAccess.get(ROLES_CLAIM);
mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
}
if (userInfo.hasClaim(RESOURCE_ACCESS_CLAIM)) {
var resourceAccess = userInfo.getClaimAsMap(RESOURCE_ACCESS_CLAIM);
if (resourceAccess.containsKey(clientId)) {
@SuppressWarnings("unchecked")
var roles = (Collection<String>) ((Map<?, ?>) resourceAccess.get(clientId)).get(ROLES_CLAIM);
mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
}
}
} else {
var oauth2UserAuthority = (OAuth2UserAuthority) authority;
Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
if (userAttributes.containsKey(REALM_ACCESS_CLAIM)) {
@SuppressWarnings("unchecked")
var realmAccess = (Map<String, Object>) userAttributes.get(REALM_ACCESS_CLAIM);
@SuppressWarnings("unchecked")
var roles = (Collection<String>) realmAccess.get(ROLES_CLAIM);
mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
}
}
return mappedAuthorities;
};
}
private Collection<GrantedAuthority> generateAuthoritiesFromClaim(Collection<String> roles) {
return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect(Collectors.toList());
}
}

View File

@ -2,20 +2,19 @@ package de.jottyfan.timetrack.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
/**
*
* @author henkej
* @author jotty
*
*/
@Configuration
@ -36,19 +35,9 @@ public class SecurityConfiguration {
.oauth2Login(o -> o.defaultSuccessUrl("/"))
.logout(o -> o.logoutSuccessHandler(new OidcClientInitiatedLogoutSuccessHandler(crr)))
.authorizeHttpRequests(o -> o.requestMatchers("/public/**").permitAll().anyRequest().authenticated())
.oauth2ResourceServer(o -> o.jwt(j -> roleConverter()))
.oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
.sessionManagement(o -> o.init(sec));
// @formatter:on
return sec.build();
}
@Bean
public JwtAuthenticationConverter roleConverter() {
JwtGrantedAuthoritiesConverter gac = new JwtGrantedAuthoritiesConverter();
gac.setAuthorityPrefix("ROLE_");
gac.setAuthoritiesClaimName("roles");
JwtAuthenticationConverter jac = new JwtAuthenticationConverter();
jac.setJwtGrantedAuthoritiesConverter(gac);
return jac;
}
}

View File

@ -20,5 +20,5 @@ spring.security.oauth2.client.provider.keycloak.jwk-set-uri = ${keycloak.openid-
spring.security.oauth2.client.provider.keycloak.user-name-attribute = preferred_username
# application
server.port = 8083
server.port = 9001
server.servlet.context-path = /timetrack

View File

@ -6,7 +6,7 @@
<body>
<font layout:fragment="title">Kalender</font>
<ul layout:fragment="menu">
<li class="nav-item" th:if="${#authentication.principal.attributes['resource_access']['timetrack']['roles'].contains('timetrack_user')}"><a class="btn btn-outline-success" th:href="@{/calendar/add}"><i class="fas fa-plus"></i></a></li>
<li class="nav-item" sec:authorize="hasRole('timetrack_user')"><a class="btn btn-outline-success" th:href="@{/calendar/add}"><i class="fas fa-plus"></i></a></li>
</ul>
<main layout:fragment="content">
<ul class="nav nav-tabs navback" role="tablist">
@ -15,7 +15,7 @@
</ul>
<div class="tabdivblurred tab-content">
<div id="div_dashboard" class="tab-pane active tab-pane-glassy">
<div th:if="${#authentication.principal.attributes['resource_access']['timetrack']['roles'].contains('timetrack_user')}">
<div sec:authorize="hasRole('timetrack_user')">
<div id="calendar"></div>
<script th:inline="javascript">
/*<![CDATA[*/
@ -43,7 +43,7 @@
</div>
</div>
<div id="div_options" class="tab-pane fade tab-pane-table">
<div th:if="${#authentication.principal.attributes['resource_access']['timetrack']['roles'].contains('timetrack_user')}">
<div sec:authorize="hasRole('timetrack_user')">
<!-- TODO: add the options here -->
</div>
</div>

View File

@ -47,7 +47,7 @@
<div class="col-sm-10">
<button type="submit" class="btn btn-success">speichern</button>
<button type="submit" class="btn btn-secondary" th:formaction="@{/contact/list}">abbrechen</button>
<div class="dropdown float-right" th:if="${contactBean.pk != null and #authentication.principal.attributes['resource_access']['timetrack']['roles'].contains('timetrack_user')}">
<div class="dropdown float-right" th:if="${contactBean.pk != null}" sec:authorize="hasRole('timetrack_user')">
<button class="btn btn-danger dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Eintrag löschen</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" th:href="@{/contact/delete/{id}(id=${contactBean.pk})}">endgültig löschen</a></li>

View File

@ -7,7 +7,7 @@
<body>
<font layout:fragment="title">Kontakte</font>
<ul layout:fragment="menu">
<li class="nav-item" th:if="${#authentication.principal.attributes['resource_access']['timetrack']['roles'].contains('timetrack_user')}"><a class="nav-link btn btn-success btn-white-text" th:href="@{/contact/add}">Neuen
<li class="nav-item" sec:authorize="hasRole('timetrack_user')"><a class="nav-link btn btn-success btn-white-text" th:href="@{/contact/add}">Neuen
Kontakt anlegen</a></li>
</ul>
<main layout:fragment="content">
@ -27,7 +27,7 @@
<div class="card-body">
<div class="d-flex justify-content-center align-items-center">
<span th:text="${contact.type} + ': ' + ${contact.contact}"></span> <a
th:href="@{/contact/edit/{id}(id=${contact.pk})}" th:if="${#authentication.principal.attributes['resource_access']['timetrack']['roles'].contains('timetrack_user')}" style="margin-left: 8px;">
th:href="@{/contact/edit/{id}(id=${contact.pk})}" sec:authorize="hasRole('timetrack_user')" style="margin-left: 8px;">
<i class="fa fa-edit"></i>
</a>
</div>

View File

@ -93,7 +93,7 @@
<div class="col-sm-10">
<button id="okbtn" type="submit" class="btn btn-success">speichern</button>
<a class="btn btn-secondary" th:href="@{/done/abort/{day}(day=${doneModel.dayString})}">abbrechen</a>
<div class="dropdown float-right" th:if="${doneBean.pk != null and #authentication.principal.attributes['resource_access']['timetrack']['roles'].contains('timetrack_user')}">
<div class="dropdown float-right" th:if="${doneBean.pk != null}" sec:authorize="hasRole('timetrack_user')">
<button class="btn btn-danger dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Eintrag löschen</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" th:href="@{/done/delete/{id}(id=${doneBean.pk})}">endgültig löschen</a></li>

View File

@ -27,7 +27,7 @@
<div class="col-sm-10">
<button id="okbtn" type="submit" class="btn btn-success">speichern</button>
<a class="btn btn-secondary" th:href="@{/done/list}">abbrechen</a>
<div class="dropdown float-right" th:if="${jobBean.pk != null and #authentication.principal.attributes['resource_access']['timetrack']['roles'].contains('timetrack_user')}">
<div class="dropdown float-right" th:if="${jobBean.pk != null}" sec:authorize="hasRole('timetrack_user')">
<button class="btn btn-danger dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Eintrag löschen</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" th:href="@{/done/delete/job/{id}(id=${jobBean.pk})}">endgültig löschen</a></li>

View File

@ -7,7 +7,7 @@
<body>
<font layout:fragment="title">Arbeitszeit</font>
<ul layout:fragment="menuitem">
<li class="nav-item" th:if="${#authentication.principal.attributes['resource_access']['timetrack']['roles'].contains('timetrack_user')}">
<li class="nav-item" sec:authorize="hasRole('timetrack_user')">
<form th:action="@{/done/list}" th:object="${doneModel}" method="post">
<div class="nav-link" style="padding-top: 5px !important; padding-bottom: 0px !important">
<div class="input-group input-group-sm mb-3" style="margin-bottom: 0px !important">
@ -19,7 +19,7 @@
</li>
</ul>
<ul layout:fragment="menu">
<li class="nav-item" th:if="${#authentication.principal.attributes['resource_access']['timetrack']['roles'].contains('timetrack_user')}">
<li class="nav-item" sec:authorize="hasRole('timetrack_user')">
<table>
<tr>
<td><a class="nav-link btn btn-success btn-white-text" th:href="@{/done/add/{day}(day=${doneModel.day})}">Neuer

View File

@ -27,7 +27,7 @@
<div class="col-sm-10">
<button id="okbtn" type="submit" class="btn btn-success">speichern</button>
<a class="btn btn-secondary" th:href="@{/done/list}">abbrechen</a>
<div class="dropdown float-right" th:if="${moduleBean.pk != null and #authentication.principal.attributes['resource_access']['timetrack']['roles'].contains('timetrack_user')}">
<div class="dropdown float-right" th:if="${moduleBean.pk != null}" sec:authorize="hasRole('timetrack_user')">
<button class="btn btn-danger dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Eintrag löschen</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" th:href="@{/done/delete/module/{id}(id=${moduleBean.pk})}">endgültig löschen</a></li>

View File

@ -27,7 +27,7 @@
<div class="col-sm-10">
<button id="okbtn" type="submit" class="btn btn-success">speichern</button>
<a class="btn btn-secondary" th:href="@{/done/list}">abbrechen</a>
<div class="dropdown float-right" th:if="${projectBean.pk != null and #authentication.principal.attributes['resource_access']['timetrack']['roles'].contains('timetrack_user')}">
<div class="dropdown float-right" th:if="${projectBean.pk != null}" sec:authorize="hasRole('timetrack_user')">
<button class="btn btn-danger dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Eintrag löschen</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" th:href="@{/done/delete/project/{id}(id=${projectBean.pk})}">endgültig löschen</a></li>

View File

@ -49,7 +49,7 @@
<div class="col-sm-10">
<button type="submit" class="btn btn-success">speichern</button>
<button type="submit" class="btn btn-secondary" th:formaction="@{/note/list}">abbrechen</button>
<div class="dropdown float-right" th:if="${noteBean.pk != null and #authentication.principal.attributes['resource_access']['timetrack']['roles'].contains('timetrack_user')}">
<div class="dropdown float-right" th:if="${noteBean.pk != null}" sec:authorize="hasRole('timetrack_user')">
<button class="btn btn-danger dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Eintrag löschen</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" th:href="@{/note/delete/{id}(id=${noteBean.pk})}">endgültig löschen</a></li>

View File

@ -7,7 +7,7 @@
<body>
<font layout:fragment="title">Notizen</font>
<ul layout:fragment="menu">
<li class="nav-item" th:if="${#authentication.principal.attributes['resource_access']['timetrack']['roles'].contains('timetrack_user')}"><a class="nav-link btn btn-success btn-white-text" th:href="@{/note/add}">Neue Notiz
<li class="nav-item" sec:authorize="hasRole('timetrack_user')"><a class="nav-link btn btn-success btn-white-text" th:href="@{/note/add}">Neue Notiz
anlegen</a></li>
</ul>
<main layout:fragment="content">
@ -27,7 +27,7 @@
<div class="card-body">
<div class="d-flex justify-content-center align-items-center">
<pre th:text="${note.content}"></pre>
<a th:href="@{/note/edit/{id}(id=${note.pk})}" th:if="${#authentication.principal.attributes['resource_access']['timetrack']['roles'].contains('timetrack_user')}" style="margin-left: 8px;">
<a th:href="@{/note/edit/{id}(id=${note.pk})}" sec:authorize="hasRole('timetrack_user')" style="margin-left: 8px;">
<i class="fa fa-edit"></i>
</a>
</div>

View File

@ -41,18 +41,6 @@
th:text="${sum.getBillingTime(null)}" class="distfat"></span>
</div>
</div>
<!-- for debugging only, set display to block -->
<div class="card" style="display: none">
<div class="card-body">
<span th:text="${#authentication.principal.attributes['resource_access']['timetrack']['roles']}"></span><br />
<span th:if="${#authentication.principal.attributes['resource_access']['timetrack']['roles'].contains('timetrack_user')}">role timetrack_user found directly</span><br />
<span sec:authorize="hasRole('timetrack_user')" style="color: green">well done</span>
<span sec:authorize="!hasRole('timetrack_user')" style="color: rgb(165, 29, 45)">role timetrack_user not yet detected</span>
<br />
<span>found roles:</span><br />
<span th:each="r : ${#authentication.principal.attributes['resource_access']['timetrack']['roles']}" th:if="${r}" th:text="${r + ' '}" style="color: royalblue"></span>
</div>
</div>
</main>
</body>