initial commit

This commit is contained in:
2021-10-03 17:07:01 +02:00
commit 456332f24e
246 changed files with 24590 additions and 0 deletions
Executable
+26
View File
@@ -0,0 +1,26 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.bstly.we</groupId>
<artifactId>webstly-main</artifactId>
<version>${revision}</version>
</parent>
<name>oidc</name>
<artifactId>webstly-oidc</artifactId>
<dependencies>
<dependency>
<groupId>de.bstly.we</groupId>
<artifactId>webstly-core</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,61 @@
/**
*
*/
package de.bstly.we.oidc.businesslogic;
import java.net.URI;
import java.util.Map;
import java.util.Set;
import org.springframework.stereotype.Service;
import com.google.common.collect.Maps;
import de.bstly.we.oidc.model.OidcAuthorizationCode;
/**
*
* @author _bastler@bstly.de
*
*/
@Service
public class OidcAuthorizationCodeManager {
/**
* hold codes in memory
*/
private final Map<String, OidcAuthorizationCode> oidcAuthorizationCodes = Maps.newHashMap();
/**
*
* @param clientId
* @param redirectUri
* @param scopes
* @param subject
* @return
*/
public OidcAuthorizationCode create(String clientId, URI redirectUri, Set<String> scopes, Long userId,
String nonce) {
OidcAuthorizationCode oidcAuthorizationCode = new OidcAuthorizationCode(clientId, redirectUri, scopes, userId,
nonce);
oidcAuthorizationCodes.put(oidcAuthorizationCode.getCode(), oidcAuthorizationCode);
return oidcAuthorizationCode;
}
/**
*
* @param code
* @return
*/
public OidcAuthorizationCode getByCode(String code) {
return oidcAuthorizationCodes.get(code);
}
/**
*
* @param code
*/
public void removeByCode(String code) {
oidcAuthorizationCodes.remove(code);
}
}
@@ -0,0 +1,179 @@
/**
*
*/
package de.bstly.we.oidc.businesslogic;
import java.util.Set;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import com.google.common.collect.Sets;
import de.bstly.we.oidc.model.OidcAuthorizationGrantType;
import de.bstly.we.oidc.model.OidcClient;
import de.bstly.we.oidc.model.OidcClientAuthenticationMethod;
import de.bstly.we.oidc.model.QOidcClient;
import de.bstly.we.oidc.repository.OidcClientRepository;
/**
*
* @author _bastler@bstly.de
*
*/
@Service
public class OidcClientManager {
public static final int OIDC_CLIENT_CLIENT_ID_LENGTH = 24;
public static final int OIDC_CLIENT_CLIENT_SECRET_LENGTH = 64;
public static final long OIDC_CLIENT_TOKEN_LIFETIME = 120;
@Autowired
private OidcClientRepository oidcClientRepository;
private QOidcClient qOidcClient = QOidcClient.oidcClient;
/**
*
* @param clientName
* @param redirectUris
* @return
*/
public OidcClient createClient(String clientName, Set<String> redirectUris) {
return createClient(clientName,
Sets.newHashSet(OidcClientAuthenticationMethod.basic,
OidcClientAuthenticationMethod.basic),
Sets.newHashSet(OidcAuthorizationGrantType.authorization_code), redirectUris,
Sets.newHashSet("openid"), null, false);
}
/**
*
* @param clientName
* @param clientAuthenticationMethods
* @param authorizationGrantTypes
* @param redirectUris
* @param scopes
* @return
*/
public OidcClient createClient(String clientName,
Set<OidcClientAuthenticationMethod> clientAuthenticationMethods,
Set<OidcAuthorizationGrantType> authorizationGrantTypes, Set<String> redirectUris,
Set<String> scopes, String loginUrl, boolean alwaysPermitted) {
OidcClient oidcClient = new OidcClient();
oidcClient.setClientName(clientName);
oidcClient.setRedirectUris(redirectUris);
oidcClient.setClientAuthenticationMethods(clientAuthenticationMethods);
oidcClient.setAuthorizationGrantTypes(authorizationGrantTypes);
oidcClient.setScopes(scopes);
oidcClient.setLoginUrl(loginUrl);
oidcClient.setAlwaysPermitted(alwaysPermitted);
oidcClient.setTokenLifetime(OIDC_CLIENT_TOKEN_LIFETIME);
String clientId = RandomStringUtils.random(OIDC_CLIENT_CLIENT_ID_LENGTH, true, true);
while (oidcClientRepository.findOne(qOidcClient.clientId.eq(clientId)).isPresent()) {
clientId = RandomStringUtils.random(OIDC_CLIENT_CLIENT_ID_LENGTH, true, true);
}
oidcClient.setClientId(clientId);
oidcClient.setClientSecret(
RandomStringUtils.random(OIDC_CLIENT_CLIENT_SECRET_LENGTH, true, true));
return oidcClientRepository.save(oidcClient);
}
/**
*
* @param oidcClient
* @return
*/
public OidcClient updateClient(OidcClient oidcClient) {
return oidcClientRepository.save(oidcClient);
}
/**
*
* @param clientName
* @return
*/
public OidcClient createNewSecretByClientName(String clientName) {
OidcClient oidcClient = getByClientName(clientName);
Assert.notNull(oidcClient, "No client found for name '" + clientName + "'");
oidcClient.setClientSecret(
RandomStringUtils.random(OIDC_CLIENT_CLIENT_SECRET_LENGTH, true, true));
return oidcClientRepository.save(oidcClient);
}
/**
*
* @param clientId
* @return
*/
public OidcClient getByClientId(String clientId) {
return oidcClientRepository.findOne(qOidcClient.clientId.eq(clientId)).orElse(null);
}
/**
*
* @param clientName
* @return
*/
public OidcClient getByClientName(String clientName) {
return oidcClientRepository.findOne(qOidcClient.clientName.eq(clientName)).orElse(null);
}
/**
*
* @param clientId
* @param clientSecret
* @return
*/
public OidcClient getByClientIdAndSecret(String clientId, String clientSecret) {
return oidcClientRepository.findOne(
qOidcClient.clientId.eq(clientId).and(qOidcClient.clientSecret.eq(clientSecret)))
.orElse(null);
}
/**
*
* @param clientId
* @return
*/
public void deleteByClientId(String clientId) {
OidcClient oidcClient = getByClientId(clientId);
if (oidcClient != null) {
oidcClientRepository.delete(oidcClient);
}
}
/**
*
* @param name
*/
public void deleteByClientName(String clientName) {
OidcClient oidcClient = getByClientName(clientName);
Assert.notNull(oidcClient, "No client found for clientName '" + clientName + "'");
oidcClientRepository.delete(oidcClient);
}
/**
*
* @param page
* @param size
* @param sortBy
* @param descending
* @return
*/
public Page<OidcClient> get(int page, int size, String sortBy, boolean descending) {
Sort sort = descending ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending();
return oidcClientRepository.findAll(PageRequest.of(page, size, sort));
}
}
@@ -0,0 +1,261 @@
/**
*
*/
package de.bstly.we.oidc.businesslogic;
import java.text.ParseException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import javax.annotation.PostConstruct;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.beust.jcommander.internal.Maps;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
import com.nimbusds.jwt.JWTClaimsSet.Builder;
import com.nimbusds.jwt.SignedJWT;
import de.bstly.we.businesslogic.PermissionManager;
import de.bstly.we.businesslogic.QuotaManager;
import de.bstly.we.businesslogic.SystemPropertyManager;
import de.bstly.we.businesslogic.UserManager;
import de.bstly.we.businesslogic.UserProfileFieldManager;
import de.bstly.we.businesslogic.UserProfileFields;
import de.bstly.we.model.Permission;
import de.bstly.we.model.Quota;
import de.bstly.we.model.User;
import de.bstly.we.model.UserProfileField;
import de.bstly.we.oidc.model.OidcClient;
import de.bstly.we.oidc.model.OidcToken;
import de.bstly.we.oidc.model.QOidcToken;
import de.bstly.we.oidc.repository.OidcTokenRepository;
/**
*
* @author _bastler@bstly.de
*
*/
@Service
public class OidcTokenManager {
public static final int ACCESS_TOKEN_LENGTH = 64;
public static final String OIDC_JWK_PUBLIC_KEY = "oidc-jwk-public-key";
public static final String BEARER_TOKEN_TYPE = "Bearer";
private RSAKey publicKey;
private JWKSet jwkSet;
private JWSSigner signer;
private JWSVerifier verifier;
@Autowired
private OidcTokenRepository tokenRepository;
@Autowired
private UserManager userManager;
@Autowired
private UserProfileFieldManager userProfileFieldManager;
@Autowired
private PermissionManager permissionManager;
@Autowired
private QuotaManager quotaManager;
@Autowired
private SystemPropertyManager systemPropertyManager;
private QOidcToken qOidcToken = QOidcToken.oidcToken;
@PostConstruct
public void initOidcTokenManager() throws JOSEException {
RSAKey rsaJWK = null;
if (systemPropertyManager.has(OIDC_JWK_PUBLIC_KEY)) {
try {
rsaJWK = RSAKey.parse(systemPropertyManager.get(OIDC_JWK_PUBLIC_KEY));
} catch (ParseException e) {
}
} else {
rsaJWK = new RSAKeyGenerator(2048).keyID("1").generate();
systemPropertyManager.add(OIDC_JWK_PUBLIC_KEY, rsaJWK.toJSONString());
}
this.publicKey = rsaJWK.toPublicJWK();
this.signer = new RSASSASigner(rsaJWK);
this.jwkSet = new JWKSet(this.publicKey);
this.verifier = new RSASSAVerifier(this.publicKey);
}
/**
*
* @param client
* @param target
* @return
*/
public OidcToken createToken(OidcClient client, Long userId) {
OidcToken token = new OidcToken();
token.setUserId(userId);
token.setAccessToken(RandomStringUtils.random(ACCESS_TOKEN_LENGTH, true, true));
token.setExpiresIn(client.getTokenLifetime());
return tokenRepository.save(token);
}
/**
*
* @param client
* @param userId
* @param nonce
* @param scopes
* @param issuer
* @return
* @throws JOSEException
*/
public OidcToken createTokenWithIdToken(OidcClient client, Long userId, String nonce,
Set<String> scopes, String issuer) throws JOSEException {
OidcToken token = new OidcToken();
token.setClient(client.getId());
User user = userManager.get(userId);
Assert.notNull(user, "User does not exist!");
token.setUserId(user.getId());
token.setAccessToken(RandomStringUtils.random(ACCESS_TOKEN_LENGTH, true, true));
token.setExpiresIn(client.getTokenLifetime());
Builder claimsSetBuilder = createUserClaims(user);
claimsSetBuilder.issuer(issuer);
claimsSetBuilder.audience(client.getClientId());
claimsSetBuilder.expirationTime(
Date.from(Instant.now().plus(client.getTokenLifetime(), ChronoUnit.SECONDS)
.atZone(ZoneId.systemDefault()).toInstant()));
claimsSetBuilder.issueTime(new Date());
if (StringUtils.hasText(nonce)) {
claimsSetBuilder.claim("nonce", nonce);
}
JWSHeader.Builder headerBuilder = new JWSHeader.Builder(JWSAlgorithm.RS256);
headerBuilder.keyID(getPublicKey().getKeyID());
SignedJWT jwt = new SignedJWT(headerBuilder.build(), claimsSetBuilder.build());
jwt.sign(getSigner());
token.setIdToken(jwt.serialize());
return tokenRepository.save(token);
}
/**
*
* @param user
* @return
*/
public Builder createUserClaims(User user) {
Builder claimsSetBuilder = new Builder();
claimsSetBuilder.subject(String.valueOf(user.getId()));
claimsSetBuilder.claim("name", user.getUsername());
claimsSetBuilder.claim("preferred_username", user.getUsername());
UserProfileField emailProfileField = userProfileFieldManager.get(user.getId(),
UserProfileFields.PROFILE_FIELD_EMAIL);
UserProfileField emailPrimaryProfileField = userProfileFieldManager.get(user.getId(),
UserProfileFields.PROFILE_FIELD_EMAIL_PRIMARY);
if (emailProfileField != null && emailPrimaryProfileField != null
&& StringUtils.hasText(emailProfileField.getValue())
&& Boolean.getBoolean(emailPrimaryProfileField.getValue())) {
claimsSetBuilder.claim("email", emailProfileField.getValue());
} else {
claimsSetBuilder.claim("email_verified", true);
claimsSetBuilder.claim("email", userManager.getBstlyEmail(user.getUsername()));
}
UserProfileField localeProfileField = userProfileFieldManager.get(user.getId(),
UserProfileFields.PROFILE_FIELD_LOCALE);
if (localeProfileField != null) {
if ("de-informal".equals(localeProfileField.getValue())) {
claimsSetBuilder.claim(UserProfileFields.PROFILE_FIELD_LOCALE, "de");
} else if (StringUtils.hasText(localeProfileField.getValue())) {
claimsSetBuilder.claim(UserProfileFields.PROFILE_FIELD_LOCALE,
localeProfileField.getValue());
}
}
Map<String, String> permissions = Maps.newHashMap();
for (Permission permission : permissionManager.getNotExpiresByTarget(user.getId())) {
permissions.put(permission.getName(), permission.getExpires().toString());
}
if (!permissions.isEmpty()) {
claimsSetBuilder.claim("permissions", permissions);
}
Map<String, String> quotas = Maps.newHashMap();
for (Quota quota : quotaManager.getNotExpiresByTarget(user.getId())) {
quotas.put(quota.getName(), String.valueOf(quota.getValue()) + quota.getUnit());
}
if (!quotas.isEmpty()) {
claimsSetBuilder.claim("quotas", quotas);
}
return claimsSetBuilder;
}
/**
*
* @param accessToken
* @return
*/
public OidcToken getByAccessToken(String accessToken) {
return tokenRepository.findOne(qOidcToken.accessToken.eq(accessToken)).orElse(null);
}
/**
*
* @return
*/
public JWSSigner getSigner() {
return signer;
}
/**
*
* @return
*/
public JWSVerifier getVerifier() {
return verifier;
}
/**
*
* @return
*/
public RSAKey getPublicKey() {
return publicKey;
}
/**
*
* @return
*/
public JWKSet getJwkSet() {
return jwkSet;
}
}
@@ -0,0 +1,176 @@
/**
*
*/
package de.bstly.we.oidc.controller;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.google.common.collect.Sets;
import de.bstly.we.controller.BaseController;
import de.bstly.we.controller.support.EntityResponseStatusException;
import de.bstly.we.oidc.businesslogic.OidcClientManager;
import de.bstly.we.oidc.controller.model.OidcClientModel;
import de.bstly.we.oidc.model.OidcAuthorizationGrantType;
import de.bstly.we.oidc.model.OidcClient;
import de.bstly.we.oidc.model.OidcClientAuthenticationMethod;
import de.bstly.we.oidc.repository.OidcClientRepository;
/**
*
* @author _bastler@bstly.de
*
*/
@RestController
@RequestMapping("/oidc/clients")
public class OIDCClientController extends BaseController {
@Autowired
private OidcClientManager registeredClientService;
@Autowired
private OidcClientRepository registeredClientRepository;
/**
*
* @param pageParameter
* @param sizeParameter
* @return
*/
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping
public Page<OidcClient> getClients(@RequestParam("page") Optional<Integer> pageParameter,
@RequestParam("size") Optional<Integer> sizeParameter) {
return registeredClientService.get(pageParameter.orElse(0), sizeParameter.orElse(10),
"clientName", true);
}
/**
*
* @param name
* @return
*/
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/{name}")
public OidcClient get(@PathVariable("name") String name) {
OidcClient client = registeredClientService.getByClientName(name);
if (client == null) {
throw new EntityResponseStatusException(HttpStatus.NO_CONTENT);
}
return client;
}
/**
*
* @param clientId
* @return
*/
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/id/{clientId}")
public OidcClient getByClientId(@PathVariable("clientId") String clientId) {
OidcClient client = registeredClientService.getByClientId(clientId);
if (client == null) {
throw new EntityResponseStatusException(HttpStatus.NO_CONTENT);
}
return client;
}
/**
*
* @param oidcClientModel
* @return
*/
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping
public OidcClient create(@RequestBody OidcClientModel oidcClientModel) {
if (registeredClientService.getByClientName(oidcClientModel.getName()) != null) {
throw new EntityResponseStatusException(HttpStatus.CONFLICT);
}
if (oidcClientModel.getClientAuthenticationMethods() == null
|| oidcClientModel.getClientAuthenticationMethods().isEmpty()) {
oidcClientModel.setClientAuthenticationMethods(Sets.newHashSet(
OidcClientAuthenticationMethod.basic, OidcClientAuthenticationMethod.post));
}
if (oidcClientModel.getAuthorizationGrantTypes() == null
|| oidcClientModel.getAuthorizationGrantTypes().isEmpty()) {
oidcClientModel.setAuthorizationGrantTypes(
Sets.newHashSet(OidcAuthorizationGrantType.authorization_code));
}
return registeredClientService.createClient(oidcClientModel.getName(),
oidcClientModel.getClientAuthenticationMethods(),
oidcClientModel.getAuthorizationGrantTypes(),
oidcClientModel.getRegisteredRedirectUris(), oidcClientModel.getScopes(),
oidcClientModel.getLoginUrl(), oidcClientModel.isAlwaysPermitted());
}
/**
*
* @param oAuth2ClientModel
* @return
*/
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PatchMapping
public OidcClient update(@RequestBody OidcClient client) {
OidcClient oldClient = registeredClientRepository.findById(client.getId()).orElse(null);
if (oldClient == null) {
throw new EntityResponseStatusException(HttpStatus.CONFLICT);
}
OidcClient otherClient = registeredClientService.getByClientName(client.getClientName());
if (otherClient != null && !otherClient.getId().equals(client.getId())) {
throw new EntityResponseStatusException(HttpStatus.CONFLICT);
}
return registeredClientService.updateClient(client);
}
/**
*
* @param oAuth2ClientModel
* @return
*/
@PreAuthorize("hasRole('ROLE_ADMIN')")
@DeleteMapping("/{name}")
public void deleteClient(@PathVariable("name") String name) {
if (registeredClientService.getByClientName(name) == null) {
throw new EntityResponseStatusException(HttpStatus.NOT_MODIFIED);
}
registeredClientService.deleteByClientName(name);
}
/**
*
* @param oAuth2ClientModel
* @return
*/
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/{name}/secret")
public OidcClient createNewSecret(@PathVariable("name") String name) {
if (registeredClientService.getByClientName(name) == null) {
throw new EntityResponseStatusException(HttpStatus.NOT_MODIFIED);
}
return registeredClientService.createNewSecretByClientName(name);
}
}
@@ -0,0 +1,284 @@
/**
*
*/
package de.bstly.we.oidc.controller;
import java.io.IOException;
import java.net.URI;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.google.common.collect.Sets;
import de.bstly.we.businesslogic.PermissionManager;
import de.bstly.we.businesslogic.Permissions;
import de.bstly.we.oidc.businesslogic.OidcAuthorizationCodeManager;
import de.bstly.we.oidc.businesslogic.OidcClientManager;
import de.bstly.we.oidc.model.OidcAuthorizationCode;
import de.bstly.we.oidc.model.OidcAuthorizationErrorCode;
import de.bstly.we.oidc.model.OidcAuthorizationGrantType;
import de.bstly.we.oidc.model.OidcAuthorizationResponseType;
import de.bstly.we.oidc.model.OidcClient;
import de.bstly.we.security.model.LocalUserDetails;
/**
*
* @author _bastler@bstly.de
*
*/
@RequestMapping("/oidc/authorize")
@RestController
public class OidcAuthorizationController {
private Logger logger = LoggerFactory.getLogger(OidcAuthorizationController.class);
@Autowired
private PermissionManager permissionManager;
@Autowired
private OidcClientManager oidcClientManager;
@Autowired
private OidcAuthorizationCodeManager oidcAuthorizationCodeManager;
/**
*
* @param scope
* @param responseType
* @param clientId
* @param redirectUri
* @param state
* @param request
* @param response
* @throws IOException
*/
@PreAuthorize("isAuthenticated()")
@GetMapping
void authorizationRequest(
// for OIDC scope must contain "openid"
@RequestParam(name = "scope") String scope,
// for now only "code" is available
@RequestParam(name = "response_type") String responseType,
// client id
@RequestParam(name = "client_id") String clientId,
// redirect url
@RequestParam(name = "redirect_uri") URI redirectUri,
// optional state
@RequestParam(name = "state", required = false) String state,
// optional state
@RequestParam(name = "nonce", required = false) String nonce,
// authentication details
@AuthenticationPrincipal LocalUserDetails principal,
// the request
HttpServletRequest request,
// the response
HttpServletResponse response) throws IOException {
if (!StringUtils.hasText(clientId)) {
logger.debug("missing client_id");
throw new InvalidAuthorizationRequestError(redirectUri,
OidcAuthorizationErrorCode.INVALID_REQUEST, "missing client_id", state);
}
OidcClient client = oidcClientManager.getByClientId(clientId);
if (client == null) {
logger.debug("invalid client_id: " + clientId);
throw new InvalidAuthorizationRequestError(redirectUri,
OidcAuthorizationErrorCode.INVALID_REQUEST, "invalid client_id", state);
}
if (!client.getRedirectUris().contains(redirectUri.toString())) {
logger.debug("invalid redirect_uri: " + redirectUri + " allowed: "
+ client.getRedirectUris());
throw new InvalidAuthorizationRequestError(redirectUri,
OidcAuthorizationErrorCode.INVALID_REQUEST, "invalid redirect_uri", state);
}
if (!client.isAlwaysPermitted()
&& !permissionManager.hasPermission(principal.getUserId(), client.getClientName())
&& !permissionManager.hasPermission(principal.getUserId(),
Permissions.ROLE_ADMIN)) {
logger.debug(
"user not allowed: " + principal.getUserId() + " - " + client.getClientName());
throw new InvalidAuthorizationRequestError(redirectUri,
OidcAuthorizationErrorCode.ACCESS_DENIED, "user not allowed", state);
}
if (!client.getAuthorizationGrantTypes()
.contains(OidcAuthorizationGrantType.authorization_code)) {
logger.debug("authorization grant type not allowed: "
+ OidcAuthorizationGrantType.authorization_code + " - "
+ client.getClientName());
throw new InvalidAuthorizationRequestError(redirectUri,
OidcAuthorizationErrorCode.UNAUTHORIZED_CLIENT,
"authorization grant type not allowed", state);
}
if (!responseType.equals(OidcAuthorizationResponseType.code.toString())) {
logger.debug("response type not allowed: " + OidcAuthorizationResponseType.code
+ " - " + client.getClientName());
throw new InvalidAuthorizationRequestError(redirectUri,
OidcAuthorizationErrorCode.UNSUPPORTED_RESPONSE_TYPE,
"response type not allowed", state);
}
Set<String> scopes = Sets.newHashSet(scope.split(" "));
if (!scopes.contains("openid")) {
logger.debug("missing openid scope: " + scopes + " - " + client.getClientName());
throw new InvalidAuthorizationRequestError(redirectUri,
OidcAuthorizationErrorCode.INVALID_SCOPE, "missing openid scope", state);
}
OidcAuthorizationCode authorizationCode = oidcAuthorizationCodeManager.create(clientId,
redirectUri, scopes, principal.getUserId(), nonce);
String uri = redirectUri.toString();
if (StringUtils.hasText(redirectUri.getQuery())) {
uri += "&code=" + authorizationCode.getCode();
} else {
uri += "?code=" + authorizationCode.getCode();
}
if (StringUtils.hasText(state)) {
uri += "&state=" + state;
}
response.sendRedirect(uri);
}
/**
*
* @param exception
* @param httpServletResponse
* @return
* @throws IOException
*/
@ExceptionHandler(InvalidAuthorizationRequestError.class)
public void handle(InvalidAuthorizationRequestError exception, HttpServletResponse response)
throws IOException {
String uri = exception.getRedirectUri().toString();
uri += "?error=" + exception.getErrorCode().getAuthorizationErrorCode();
if (StringUtils.hasText(exception.getErrorDescription())) {
uri += "&error_description=" + exception.getErrorDescription();
}
if (StringUtils.hasText(exception.getState())) {
uri += "&state=" + exception.getState();
}
response.sendRedirect(uri);
}
/**
*
* @author _bastler@bstly.de
*
*/
static class InvalidAuthorizationRequestError extends RuntimeException {
/**
* default serialVersionUID
*/
private static final long serialVersionUID = 1L;
private URI redirectUri;
private OidcAuthorizationErrorCode errorCode;
private String errorDescription;
private String state;
/**
*
* @param redirectUri
* @param errorCode
* @param errorDescription
* @param state
*/
InvalidAuthorizationRequestError(URI redirectUri, OidcAuthorizationErrorCode errorCode,
String errorDescription, String state) {
super(errorDescription);
this.redirectUri = redirectUri;
this.errorCode = errorCode;
this.errorDescription = errorDescription;
this.state = state;
}
/**
* @return the redirectUri
*/
public URI getRedirectUri() {
return redirectUri;
}
/**
* @param redirectUri the redirectUri to set
*/
public void setRedirectUri(URI redirectUri) {
this.redirectUri = redirectUri;
}
/**
* @return the errorCode
*/
public OidcAuthorizationErrorCode getErrorCode() {
return errorCode;
}
/**
* @param errorCode the errorCode to set
*/
public void setErrorCode(OidcAuthorizationErrorCode errorCode) {
this.errorCode = errorCode;
}
/**
* @return the errorDescription
*/
public String getErrorDescription() {
return errorDescription;
}
/**
* @param errorDescription the errorDescription to set
*/
public void setErrorDescription(String errorDescription) {
this.errorDescription = errorDescription;
}
/**
* @return the state
*/
public String getState() {
return state;
}
/**
* @param state the state to set
*/
public void setState(String state) {
this.state = state;
}
}
}
@@ -0,0 +1,69 @@
/**
*
*/
package de.bstly.we.oidc.controller;
import java.net.URI;
import java.net.URISyntaxException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.google.common.collect.Sets;
import de.bstly.we.controller.support.EntityResponseStatusException;
import de.bstly.we.oidc.model.OidcConfiguration;
/**
* @author _bastler@bstly.de
*
*/
@RequestMapping("/.well-known/openid-configuration")
@RestController
public class OidcDiscoveryController {
@Value("${oidc.provider.issuer:}")
private String oidcIssuer;
@GetMapping
public OidcConfiguration getConfiguration(HttpServletRequest request,
HttpServletResponse response) {
OidcConfiguration config = new OidcConfiguration();
String issuer = oidcIssuer;
if (!StringUtils.hasText(issuer)) {
issuer = request.getScheme() + "://" + request.getServerName();
if (request.getServerPort() != 443 && request.getServerPort() != 80) {
issuer += ":" + request.getServerPort();
}
}
config.setIssuer(issuer);
config.setScopes_supported(Sets.newHashSet("openid"));
config.setResponse_types_supported(Sets.newHashSet("code"));
config.setGrant_types_supported(Sets.newHashSet("authorization_code"));
config.setToken_endpoint_auth_methods_supported(
Sets.newHashSet("client_secret_post", "client_secret_basic"));
config.setSubject_types_supported(Sets.newHashSet("public"));
try {
config.setAuthorization_endpoint(new URI(issuer + "/oidc/authorize"));
config.setToken_endpoint(new URI(issuer + "/oidc/token"));
config.setUserinfo_endpoint(new URI(issuer + "/oidc/userinfo"));
config.setJwks_uri(new URI(issuer + "/oidc/jwks"));
} catch (URISyntaxException e) {
throw new EntityResponseStatusException(HttpStatus.CONFLICT);
}
return config;
}
}
@@ -0,0 +1,31 @@
/**
*
*/
package de.bstly.we.oidc.controller;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import de.bstly.we.oidc.businesslogic.OidcTokenManager;
/**
*
* @author _bastler@bstly.de
*
*/
@RequestMapping("/oidc/jwks")
@RestController
public class OidcJwksController {
@Autowired
private OidcTokenManager oidcTokenManager;
@GetMapping
public Map<String, Object> getJwks() {
return oidcTokenManager.getJwkSet().toJSONObject();
}
}
@@ -0,0 +1,265 @@
/**
*
*/
package de.bstly.we.oidc.controller;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import com.google.common.collect.Sets;
import com.nimbusds.jose.JOSEException;
import de.bstly.we.oidc.businesslogic.OidcAuthorizationCodeManager;
import de.bstly.we.oidc.businesslogic.OidcClientManager;
import de.bstly.we.oidc.businesslogic.OidcTokenManager;
import de.bstly.we.oidc.model.OidcAuthorizationCode;
import de.bstly.we.oidc.model.OidcClient;
import de.bstly.we.oidc.model.OidcClientAuthenticationMethod;
import de.bstly.we.oidc.model.OidcToken;
import de.bstly.we.oidc.model.OidcTokenErrorCode;
import de.bstly.we.oidc.model.OidcTokenRequest;
import de.bstly.we.oidc.model.OidcTokenResponse;
/**
*
* @author _bastler@bstly.de
*
*/
@RequestMapping("/oidc/token")
@RestController
public class OidcTokenController {
private Logger logger = LoggerFactory.getLogger(OidcTokenController.class);
public static final String BASIC_AUTH = "Basic";
@Autowired
private OidcClientManager oidcClientManager;
@Autowired
private OidcAuthorizationCodeManager oidcAuthorizationCodeManager;
@Autowired
private OidcTokenManager oidcTokenManager;
@Value("${oidc.provider.issuer:}")
private String oidcIssuer;
@PostMapping
public OidcTokenResponse getToken(
// Authorization header for BASIC client authentication method
@RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String authorizationHeader,
// token request
@ModelAttribute("tokenRequest") OidcTokenRequest tokenRequest,
// the request
HttpServletRequest request,
// the response
HttpServletResponse response) {
response.setHeader(HttpHeaders.CACHE_CONTROL, "no-store");
response.setHeader(HttpHeaders.PRAGMA, "no-cache");
OidcClientAuthenticationMethod clientAuthenticationMethod = OidcClientAuthenticationMethod.post;
if (StringUtils.hasText(authorizationHeader)
&& StringUtils.startsWithIgnoreCase(authorizationHeader, BASIC_AUTH)) {
String decoded = new String(Base64.getDecoder().decode(authorizationHeader
.replaceFirst(BASIC_AUTH, "").trim().getBytes(StandardCharsets.UTF_8)),
StandardCharsets.UTF_8);
if (decoded.indexOf(":") != -1) {
tokenRequest.setClient_id(decoded.split(":")[0]);
tokenRequest.setClient_secret(decoded.split(":")[1]);
clientAuthenticationMethod = OidcClientAuthenticationMethod.basic;
} else {
logger.debug("invalid_basic_authentication: " + decoded);
throw new InvalidTokenRequestError(OidcTokenErrorCode.INVALID_CLIENT,
"invalid_basic_authentication");
}
}
OidcClient client = oidcClientManager.getByClientIdAndSecret(tokenRequest.getClient_id(),
tokenRequest.getClient_secret());
if (client == null) {
logger.debug("client not found: " + tokenRequest.getClient_id());
throw new InvalidTokenRequestError(OidcTokenErrorCode.INVALID_CLIENT, "invalid_client");
}
if (!client.getClientAuthenticationMethods().contains(clientAuthenticationMethod)) {
logger.debug("invalid_authentication_method: " + clientAuthenticationMethod);
throw new InvalidTokenRequestError(OidcTokenErrorCode.INVALID_REQUEST,
"invalid_authentication_method");
}
if (!client.getAuthorizationGrantTypes().contains(tokenRequest.getGrant_type())) {
logger.debug("invalid_grant_type: " + tokenRequest.getGrant_type());
throw new InvalidTokenRequestError(OidcTokenErrorCode.UNAUTHORIZED_CLIENT,
"invalid_grant_type");
}
if (tokenRequest.getRedirect_uri() != null
&& !client.getRedirectUris().contains(tokenRequest.getRedirect_uri().toString())) {
logger.debug("invalid redirect_uri: " + tokenRequest.getRedirect_uri().toString()
+ " allowed: " + client.getRedirectUris());
throw new InvalidTokenRequestError(OidcTokenErrorCode.INVALID_REQUEST,
"invalid_redirect_uri");
}
OidcToken token = null;
switch (tokenRequest.getGrant_type()) {
case authorization_code:
OidcAuthorizationCode authorizationCode = oidcAuthorizationCodeManager
.getByCode(tokenRequest.getCode());
if (authorizationCode == null) {
logger.debug("invalid authorization code: " + tokenRequest.getCode());
throw new InvalidTokenRequestError(OidcTokenErrorCode.INVALID_GRANT,
"invalid_authorization_code");
}
if (Instant.now().isAfter(authorizationCode.getExpiry())) {
logger.debug("authorization code expired: " + authorizationCode.getExpiry());
throw new InvalidTokenRequestError(OidcTokenErrorCode.INVALID_GRANT,
"invalid_authorization_code");
}
if (!tokenRequest.getClient_id().equals(authorizationCode.getClientId())) {
logger.debug("invalid client for authorization code, expected: "
+ authorizationCode.getClientId() + " got: " + tokenRequest.getClient_id());
throw new InvalidTokenRequestError(OidcTokenErrorCode.INVALID_CLIENT,
"invalid_client");
}
Set<String> scopes = StringUtils.hasText(tokenRequest.getScope())
? Sets.newHashSet(tokenRequest.getScope().split(" "))
: authorizationCode.getScopes();
if (!scopes.contains("openid") || !client.getScopes().containsAll(scopes)) {
logger.debug("missing openid scope: " + scopes + " - " + client.getClientName());
throw new InvalidTokenRequestError(OidcTokenErrorCode.INVALID_SCOPE,
"invalid scopes");
}
String issuer = oidcIssuer;
if (!StringUtils.hasText(issuer)) {
issuer = request.getScheme() + "://" + request.getServerName();
if (request.getServerPort() != 443 && request.getServerPort() != 80) {
issuer += ":" + request.getServerPort();
}
}
try {
token = oidcTokenManager.createTokenWithIdToken(client,
authorizationCode.getUserId(), authorizationCode.getNonce(), scopes,
issuer);
} catch (JOSEException e) {
logger.error("error creating token", client, authorizationCode);
e.printStackTrace();
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
}
oidcAuthorizationCodeManager.removeByCode(tokenRequest.getCode());
break;
case client_credentials:
token = oidcTokenManager.createToken(client, client.getId());
break;
}
OidcTokenResponse tokenResponse = new OidcTokenResponse();
tokenResponse.setAccess_token(token.getAccessToken());
tokenResponse.setId_token(token.getIdToken());
tokenResponse.setToken_type(OidcTokenManager.BEARER_TOKEN_TYPE);
tokenResponse.setExpires_in(client.getTokenLifetime());
oidcAuthorizationCodeManager.removeByCode(tokenRequest.getCode());
return tokenResponse;
}
/**
*
* @param exception
* @param httpServletResponse
* @return
* @throws IOException
*/
@ExceptionHandler(InvalidTokenRequestError.class)
public ResponseEntity<String> handle(InvalidTokenRequestError exception,
HttpServletResponse response) throws IOException {
// response.sendError(400, "redirect uri mismatch");
return ResponseEntity.badRequest().contentType(MediaType.APPLICATION_JSON)
.body(" {\"error\": \"" + exception.getMessage() + "\"}");
}
/**
*
* @author _bastler@bstly.de
*
*/
static class InvalidTokenRequestError extends RuntimeException {
/**
* default serialVersionUID
*/
private static final long serialVersionUID = 1L;
private OidcTokenErrorCode errorCode;
private String errorDescription;
InvalidTokenRequestError(OidcTokenErrorCode errorCode, String errorDescription) {
super(errorDescription);
this.errorCode = errorCode;
this.errorDescription = errorDescription;
}
/**
* @return the errorCode
*/
public OidcTokenErrorCode getErrorCode() {
return errorCode;
}
/**
* @param errorCode the errorCode to set
*/
public void setErrorCode(OidcTokenErrorCode errorCode) {
this.errorCode = errorCode;
}
/**
* @return the errorDescription
*/
public String getErrorDescription() {
return errorDescription;
}
/**
* @param errorDescription the errorDescription to set
*/
public void setErrorDescription(String errorDescription) {
this.errorDescription = errorDescription;
}
}
}
@@ -0,0 +1,75 @@
/**
*
*/
package de.bstly.we.oidc.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.google.common.net.HttpHeaders;
import com.nimbusds.jwt.JWTClaimsSet.Builder;
import de.bstly.we.businesslogic.UserManager;
import de.bstly.we.controller.BaseController;
import de.bstly.we.controller.support.EntityResponseStatusException;
import de.bstly.we.model.User;
import de.bstly.we.oidc.businesslogic.OidcTokenManager;
import de.bstly.we.oidc.model.OidcToken;
/**
*
* @author _bastler@bstly.de
*
*/
@RequestMapping("/oidc/userinfo")
@RestController
public class OidcUserInfoController extends BaseController {
@Autowired
private OidcTokenManager oidcTokenManager;
@Autowired
private UserManager userManager;
/**
*
* @param authorizationHeader
* @return
*/
@RequestMapping(method = { RequestMethod.GET, RequestMethod.POST })
public void getUserInfo(
@RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String authorizationHeader) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Long userId = getCurrentUserId();
if (!auth.isAuthenticated()) {
if (authorizationHeader == null) {
throw new EntityResponseStatusException(HttpStatus.UNAUTHORIZED);
}
String accessToken = authorizationHeader.replaceFirst(OidcTokenManager.BEARER_TOKEN_TYPE, "").trim();
OidcToken token = oidcTokenManager.getByAccessToken(accessToken);
if (token == null) {
throw new EntityResponseStatusException(HttpStatus.UNAUTHORIZED);
}
userId = token.getUserId();
}
User user = userManager.get(userId);
if (user == null) {
throw new EntityResponseStatusException(HttpStatus.CONFLICT);
}
Builder claimsSetBuilder = oidcTokenManager.createUserClaims(user);
throw new EntityResponseStatusException(claimsSetBuilder.build().toJSONObject(), HttpStatus.OK);
}
}
@@ -0,0 +1,126 @@
/**
*
*/
package de.bstly.we.oidc.controller.model;
import java.util.Set;
import de.bstly.we.oidc.model.OidcAuthorizationGrantType;
import de.bstly.we.oidc.model.OidcClientAuthenticationMethod;
/**
*
* @author _bastler@bstly.de
*
*/
public class OidcClientModel {
private String name;
private Set<String> registeredRedirectUris;
private Set<OidcClientAuthenticationMethod> clientAuthenticationMethods;
private Set<OidcAuthorizationGrantType> authorizationGrantTypes;
private Set<String> scopes;
private String loginUrl;
private boolean alwaysPermitted;
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the registeredRedirectUris
*/
public Set<String> getRegisteredRedirectUris() {
return registeredRedirectUris;
}
/**
* @param registeredRedirectUris the registeredRedirectUris to set
*/
public void setRegisteredRedirectUris(Set<String> registeredRedirectUris) {
this.registeredRedirectUris = registeredRedirectUris;
}
/**
* @return the clientAuthenticationMethods
*/
public Set<OidcClientAuthenticationMethod> getClientAuthenticationMethods() {
return clientAuthenticationMethods;
}
/**
* @param clientAuthenticationMethods the clientAuthenticationMethods to set
*/
public void setClientAuthenticationMethods(
Set<OidcClientAuthenticationMethod> clientAuthenticationMethods) {
this.clientAuthenticationMethods = clientAuthenticationMethods;
}
/**
* @return the authorizationGrantTypes
*/
public Set<OidcAuthorizationGrantType> getAuthorizationGrantTypes() {
return authorizationGrantTypes;
}
/**
* @param authorizationGrantTypes the authorizationGrantTypes to set
*/
public void setAuthorizationGrantTypes(
Set<OidcAuthorizationGrantType> authorizationGrantTypes) {
this.authorizationGrantTypes = authorizationGrantTypes;
}
/**
* @return the scopes
*/
public Set<String> getScopes() {
return scopes;
}
/**
* @param scopes the scopes to set
*/
public void setScopes(Set<String> scopes) {
this.scopes = scopes;
}
/**
* @return the loginUrl
*/
public String getLoginUrl() {
return loginUrl;
}
/**
* @param loginUrl the loginUrl to set
*/
public void setLoginUrl(String loginUrl) {
this.loginUrl = loginUrl;
}
/**
* @return the alwaysPermitted
*/
public boolean isAlwaysPermitted() {
return alwaysPermitted;
}
/**
* @param alwaysPermitted the alwaysPermitted to set
*/
public void setAlwaysPermitted(boolean alwaysPermitted) {
this.alwaysPermitted = alwaysPermitted;
}
}
@@ -0,0 +1,99 @@
/**
*
*/
package de.bstly.we.oidc.model;
import java.net.URI;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Set;
import org.apache.commons.lang3.RandomStringUtils;
/**
*
* @author _bastler@bstly.de
*
*/
public class OidcAuthorizationCode {
public static final int CODE_LENGTH = 32;
public static final int EXPIRY_MINUTES = 2;
private final String clientId;
private final URI redirectUri;
private final Set<String> scopes;
private final String code;
private final Instant expiry;
private final Long userId;
private final String nonce;
/**
* @param clientId
* @param redirectUri
* @param scopes
* @param code
* @param expiry
* @param subject
*/
public OidcAuthorizationCode(String clientId, URI redirectUri, Set<String> scopes, Long userId,
String nonce) {
this.clientId = clientId;
this.redirectUri = redirectUri;
this.scopes = scopes;
this.code = RandomStringUtils.random(CODE_LENGTH, true, true);
this.expiry = Instant.now().plus(EXPIRY_MINUTES, ChronoUnit.MINUTES);
this.userId = userId;
this.nonce = nonce;
}
/**
* @return the clientId
*/
public String getClientId() {
return clientId;
}
/**
* @return the redirectUri
*/
public URI getRedirectUri() {
return redirectUri;
}
/**
* @return the scopes
*/
public Set<String> getScopes() {
return scopes;
}
/**
* @return the code
*/
public String getCode() {
return code;
}
/**
* @return the expiry
*/
public Instant getExpiry() {
return expiry;
}
/**
* @return the userId
*/
public Long getUserId() {
return userId;
}
/**
* @return the nonce
*/
public String getNonce() {
return nonce;
}
}
@@ -0,0 +1,30 @@
/**
*
*/
package de.bstly.we.oidc.model;
/**
*
* @author _bastler@bstly.de
*
*/
public enum OidcAuthorizationErrorCode {
INVALID_REQUEST("invalid_request"), UNAUTHORIZED_CLIENT("unauthorized_client"), ACCESS_DENIED("access_denied"),
UNSUPPORTED_RESPONSE_TYPE("unsupported_response_type"), INVALID_SCOPE("invalid_scope"),
SERVER_ERROR("server_error"), TEMPORARILY_UNAVAILABLE("temporarily_unavailable"),
INTERACTION_REQUIRED("interaction_required"), LOGIN_REQUIRED("login_required"),
ACCOUNT_SELECTION_REQUIRED("account_selection_required"), CONSENT_REQUIRED("consent_required"),
INVALID_REQUEST_URI("invalid_request_uri"), INVALID_REQUEST_OBJECT("invalid_request_object"),
REQUEST_NOT_SUPPORTED("request_not_supported"), REQUEST_URI_NOT_SUPPORTED("invalid_request"),
REGISTRATION_NOT_SUPPORTED("registration_not_supported");
private final String authorizationErrorCode;
OidcAuthorizationErrorCode(String authorizationErrorCode) {
this.authorizationErrorCode = authorizationErrorCode;
}
public String getAuthorizationErrorCode() {
return authorizationErrorCode;
}
}
@@ -0,0 +1,13 @@
/**
*
*/
package de.bstly.we.oidc.model;
/**
*
* @author _bastler@bstly.de
*
*/
public enum OidcAuthorizationGrantType {
authorization_code, client_credentials
}
@@ -0,0 +1,13 @@
/**
*
*/
package de.bstly.we.oidc.model;
/**
*
* @author _bastler@bstly.de
*
*/
public enum OidcAuthorizationResponseType {
code
}
@@ -0,0 +1,238 @@
/**
*
*/
package de.bstly.we.oidc.model;
import java.util.Set;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import org.hibernate.annotations.LazyCollection;
import org.hibernate.annotations.LazyCollectionOption;
/**
*
* @author _bastler@bstly.de
*
*/
@Entity
@Table(name = "oidc_clients")
public class OidcClient {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "client_name", unique = true)
private String clientName;
@Column(name = "client_id", unique = true)
private String clientId;
@Column(name = "client_secret")
private String clientSecret;
@ElementCollection
@LazyCollection(LazyCollectionOption.FALSE)
@CollectionTable(name = "oidc_clients_methods")
@Enumerated(EnumType.STRING)
private Set<OidcClientAuthenticationMethod> clientAuthenticationMethods;
@ElementCollection
@LazyCollection(LazyCollectionOption.FALSE)
@CollectionTable(name = "oidc_clients_grant_types")
@Enumerated(EnumType.STRING)
private Set<OidcAuthorizationGrantType> authorizationGrantTypes;
@ElementCollection
@LazyCollection(LazyCollectionOption.FALSE)
@CollectionTable(name = "oidc_clients_redirect_uris")
private Set<String> redirectUris;
@ElementCollection
@LazyCollection(LazyCollectionOption.FALSE)
@CollectionTable(name = "oidc_clients_scopes")
private Set<String> scopes;
@Column(name = "token_lifetime")
private Long tokenLifetime;
@Column(name = "login_url", length = 1024)
private String loginUrl;
@Column(name = "always_permitted", columnDefinition = "boolean default false")
private boolean alwaysPermitted;
@Column(name = "category")
private String category;
/**
* @return the id
*/
public Long getId() {
return id;
}
/**
* @param id the id to set
*/
public void setId(Long id) {
this.id = id;
}
/**
* @return the clientName
*/
public String getClientName() {
return clientName;
}
/**
* @param clientName the clientName to set
*/
public void setClientName(String clientName) {
this.clientName = clientName;
}
/**
* @return the clientId
*/
public String getClientId() {
return clientId;
}
/**
* @param clientId the clientId to set
*/
public void setClientId(String clientId) {
this.clientId = clientId;
}
/**
* @return the clientSecret
*/
public String getClientSecret() {
return clientSecret;
}
/**
* @param clientSecret the clientSecret to set
*/
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
/**
* @return the clientAuthenticationMethods
*/
public Set<OidcClientAuthenticationMethod> getClientAuthenticationMethods() {
return clientAuthenticationMethods;
}
/**
* @param clientAuthenticationMethods the clientAuthenticationMethods to set
*/
public void setClientAuthenticationMethods(
Set<OidcClientAuthenticationMethod> clientAuthenticationMethods) {
this.clientAuthenticationMethods = clientAuthenticationMethods;
}
/**
* @return the authorizationGrantTypes
*/
public Set<OidcAuthorizationGrantType> getAuthorizationGrantTypes() {
return authorizationGrantTypes;
}
/**
* @param authorizationGrantTypes the authorizationGrantTypes to set
*/
public void setAuthorizationGrantTypes(
Set<OidcAuthorizationGrantType> authorizationGrantTypes) {
this.authorizationGrantTypes = authorizationGrantTypes;
}
/**
* @return the redirectUris
*/
public Set<String> getRedirectUris() {
return redirectUris;
}
/**
* @param redirectUris the redirectUris to set
*/
public void setRedirectUris(Set<String> redirectUris) {
this.redirectUris = redirectUris;
}
/**
* @return the scopes
*/
public Set<String> getScopes() {
return scopes;
}
/**
* @param scopes the scopes to set
*/
public void setScopes(Set<String> scopes) {
this.scopes = scopes;
}
/**
* @return the tokenLifetime
*/
public Long getTokenLifetime() {
return tokenLifetime;
}
/**
* @param tokenLifetime the tokenLifetime to set
*/
public void setTokenLifetime(Long tokenLifetime) {
this.tokenLifetime = tokenLifetime;
}
/**
* @return the loginUrl
*/
public String getLoginUrl() {
return loginUrl;
}
/**
* @param loginUrl the loginUrl to set
*/
public void setLoginUrl(String loginUrl) {
this.loginUrl = loginUrl;
}
/**
* @return the alwaysPermitted
*/
public boolean isAlwaysPermitted() {
return alwaysPermitted;
}
/**
* @param alwaysPermitted the alwaysPermitted to set
*/
public void setAlwaysPermitted(boolean alwaysPermitted) {
this.alwaysPermitted = alwaysPermitted;
}
/**
* @return the category
*/
public String getCategory() {
return category;
}
/**
* @param category the category to set
*/
public void setCategory(String category) {
this.category = category;
}
}
@@ -0,0 +1,13 @@
/**
*
*/
package de.bstly.we.oidc.model;
/**
*
* @author _bastler@bstly.de
*
*/
public enum OidcClientAuthenticationMethod {
basic, post
}
@@ -0,0 +1,169 @@
/**
*
*/
package de.bstly.we.oidc.model;
import java.net.URI;
import java.util.Set;
/**
* @author _bastler@bstly.de
*
*/
public class OidcConfiguration {
private String issuer;
private URI authorization_endpoint;
private URI token_endpoint;
private URI userinfo_endpoint;
private URI jwks_uri;
private Set<String> scopes_supported;
private Set<String> response_types_supported;
private Set<String> subject_types_supported;
private Set<String> grant_types_supported;
private Set<String> token_endpoint_auth_methods_supported;
/**
* @return the issuer
*/
public String getIssuer() {
return issuer;
}
/**
* @param issuer the issuer to set
*/
public void setIssuer(String issuer) {
this.issuer = issuer;
}
/**
* @return the authorization_endpoint
*/
public URI getAuthorization_endpoint() {
return authorization_endpoint;
}
/**
* @param authorization_endpoint the authorization_endpoint to set
*/
public void setAuthorization_endpoint(URI authorization_endpoint) {
this.authorization_endpoint = authorization_endpoint;
}
/**
* @return the token_endpoint
*/
public URI getToken_endpoint() {
return token_endpoint;
}
/**
* @param token_endpoint the token_endpoint to set
*/
public void setToken_endpoint(URI token_endpoint) {
this.token_endpoint = token_endpoint;
}
/**
* @return the userinfo_endpoint
*/
public URI getUserinfo_endpoint() {
return userinfo_endpoint;
}
/**
* @param userinfo_endpoint the userinfo_endpoint to set
*/
public void setUserinfo_endpoint(URI userinfo_endpoint) {
this.userinfo_endpoint = userinfo_endpoint;
}
/**
* @return the jwks_uri
*/
public URI getJwks_uri() {
return jwks_uri;
}
/**
* @param jwks_uri the jwks_uri to set
*/
public void setJwks_uri(URI jwks_uri) {
this.jwks_uri = jwks_uri;
}
/**
* @return the scopes_supported
*/
public Set<String> getScopes_supported() {
return scopes_supported;
}
/**
* @param scopes_supported the scopes_supported to set
*/
public void setScopes_supported(Set<String> scopes_supported) {
this.scopes_supported = scopes_supported;
}
/**
* @return the response_types_supported
*/
public Set<String> getResponse_types_supported() {
return response_types_supported;
}
/**
* @param response_types_supported the response_types_supported to set
*/
public void setResponse_types_supported(Set<String> response_types_supported) {
this.response_types_supported = response_types_supported;
}
/**
* @return the subject_types_supported
*/
public Set<String> getSubject_types_supported() {
return subject_types_supported;
}
/**
* @param subject_types_supported the subject_types_supported to set
*/
public void setSubject_types_supported(Set<String> subject_types_supported) {
this.subject_types_supported = subject_types_supported;
}
/**
* @return the grant_types_supported
*/
public Set<String> getGrant_types_supported() {
return grant_types_supported;
}
/**
* @param grant_types_supported the grant_types_supported to set
*/
public void setGrant_types_supported(Set<String> grant_types_supported) {
this.grant_types_supported = grant_types_supported;
}
/**
* @return the token_endpoint_auth_methods_supported
*/
public Set<String> getToken_endpoint_auth_methods_supported() {
return token_endpoint_auth_methods_supported;
}
/**
* @param token_endpoint_auth_methods_supported the
* token_endpoint_auth_methods_supported
* to set
*/
public void setToken_endpoint_auth_methods_supported(
Set<String> token_endpoint_auth_methods_supported) {
this.token_endpoint_auth_methods_supported = token_endpoint_auth_methods_supported;
}
}
@@ -0,0 +1,163 @@
/**
*
*/
package de.bstly.we.oidc.model;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import org.hibernate.annotations.LazyCollection;
import org.hibernate.annotations.LazyCollectionOption;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
*
* @author _bastler@bstly.de
*
*/
@Entity
@Table(name = "oidc_tokens")
public class OidcToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "user_id")
private Long userId;
@Column(name = "client_id")
private Long client;
@Column(name = "access_token")
private String accessToken;
@Column(name = "refresh_token")
private String refreshToken;
@Column(name = "expires_in")
private Long expiresIn;
@Column(name = "id_token", length = 4000)
private String idToken;
@JsonIgnore
@ElementCollection
@LazyCollection(LazyCollectionOption.FALSE)
private Set<String> scopes;
/**
* @return the id
*/
public Long getId() {
return id;
}
/**
* @param id the id to set
*/
public void setId(Long id) {
this.id = id;
}
/**
* @return the userId
*/
public Long getUserId() {
return userId;
}
/**
* @param userId the userId to set
*/
public void setUserId(Long userId) {
this.userId = userId;
}
/**
* @return the client
*/
public Long getClient() {
return client;
}
/**
* @param client the client to set
*/
public void setClient(Long client) {
this.client = client;
}
/**
* @return the accessToken
*/
public String getAccessToken() {
return accessToken;
}
/**
* @param accessToken the accessToken to set
*/
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
/**
* @return the refreshToken
*/
public String getRefreshToken() {
return refreshToken;
}
/**
* @param refreshToken the refreshToken to set
*/
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
/**
* @return the expiresIn
*/
public Long getExpiresIn() {
return expiresIn;
}
/**
* @param expiresIn the expiresIn to set
*/
public void setExpiresIn(Long expiresIn) {
this.expiresIn = expiresIn;
}
/**
* @return the idToken
*/
public String getIdToken() {
return idToken;
}
/**
* @param idToken the idToken to set
*/
public void setIdToken(String idToken) {
this.idToken = idToken;
}
/**
* @return the scopes
*/
public Set<String> getScopes() {
return scopes;
}
/**
* @param scopes the scopes to set
*/
public void setScopes(Set<String> scopes) {
this.scopes = scopes;
}
}
@@ -0,0 +1,25 @@
/**
*
*/
package de.bstly.we.oidc.model;
/**
*
* @author _bastler@bstly.de
*
*/
public enum OidcTokenErrorCode {
INVALID_REQUEST("invalid_request"), INVALID_CLIENT("invalid_client"),
INVALID_GRANT("invalid_grant"), UNAUTHORIZED_CLIENT("unauthorized_client"),
UNSUPPORTED_GRANT_TYPE("unsupported_grant_type"), INVALID_SCOPE("invalid_scope");
private final String tokenErrorCode;
OidcTokenErrorCode(String tokenErrorCode) {
this.tokenErrorCode = tokenErrorCode;
}
public String getTokenErrorCode() {
return tokenErrorCode;
}
}
@@ -0,0 +1,97 @@
/**
*
*/
package de.bstly.we.oidc.model;
import java.net.URI;
/**
*
* @author _bastler@bstly.de
*
*/
public class OidcTokenRequest {
private final String code;
private final OidcAuthorizationGrantType grant_type;
private String client_id;
private String client_secret;
private final URI redirect_uri;
private final String scope;
/**
* @param code
* @param grant_type
* @param client_id
* @param client_secret
* @param redirect_uri
* @param scope
*/
public OidcTokenRequest(String code, OidcAuthorizationGrantType grant_type, String client_id,
String client_secret, URI redirect_uri, String scope) {
super();
this.code = code;
this.grant_type = grant_type;
this.client_id = client_id;
this.client_secret = client_secret;
this.redirect_uri = redirect_uri;
this.scope = scope;
}
/**
* @return the client_id
*/
public String getClient_id() {
return client_id;
}
/**
* @param client_id the client_id to set
*/
public void setClient_id(String client_id) {
this.client_id = client_id;
}
/**
* @return the client_secret
*/
public String getClient_secret() {
return client_secret;
}
/**
* @param client_secret the client_secret to set
*/
public void setClient_secret(String client_secret) {
this.client_secret = client_secret;
}
/**
* @return the code
*/
public String getCode() {
return code;
}
/**
* @return the grant_type
*/
public OidcAuthorizationGrantType getGrant_type() {
return grant_type;
}
/**
* @return the redirect_uri
*/
public URI getRedirect_uri() {
return redirect_uri;
}
/**
* @return the scope
*/
public String getScope() {
return scope;
}
}
@@ -0,0 +1,89 @@
/**
*
*/
package de.bstly.we.oidc.model;
/**
*
* @author _bastler@bstly.de
*
*/
public class OidcTokenResponse {
private String access_token;
private String token_type;
private String refresh_token;
private long expires_in;
private String id_token;
/**
* @return the access_token
*/
public String getAccess_token() {
return access_token;
}
/**
* @param access_token the access_token to set
*/
public void setAccess_token(String access_token) {
this.access_token = access_token;
}
/**
* @return the token_type
*/
public String getToken_type() {
return token_type;
}
/**
* @param token_type the token_type to set
*/
public void setToken_type(String token_type) {
this.token_type = token_type;
}
/**
* @return the refresh_token
*/
public String getRefresh_token() {
return refresh_token;
}
/**
* @param refresh_token the refresh_token to set
*/
public void setRefresh_token(String refresh_token) {
this.refresh_token = refresh_token;
}
/**
* @return the expires_in
*/
public long getExpires_in() {
return expires_in;
}
/**
* @param expires_in the expires_in to set
*/
public void setExpires_in(long expires_in) {
this.expires_in = expires_in;
}
/**
* @return the id_token
*/
public String getId_token() {
return id_token;
}
/**
* @param id_token the id_token to set
*/
public void setId_token(String id_token) {
this.id_token = id_token;
}
}
@@ -0,0 +1,19 @@
/**
*
*/
package de.bstly.we.oidc.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.stereotype.Repository;
import de.bstly.we.oidc.model.OidcClient;
/**
*
* @author _bastler@bstly.de
*
*/
@Repository
public interface OidcClientRepository extends JpaRepository<OidcClient, Long>, QuerydslPredicateExecutor<OidcClient> {
}
@@ -0,0 +1,19 @@
/**
*
*/
package de.bstly.we.oidc.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.stereotype.Repository;
import de.bstly.we.oidc.model.OidcToken;
/**
*
* @author _bastler@bstly.de
*
*/
@Repository
public interface OidcTokenRepository extends JpaRepository<OidcToken, Long>, QuerydslPredicateExecutor<OidcToken> {
}