🚧 Add refresh grant
This commit is contained in:
parent
63d21eaf39
commit
6804ebd46a
@ -1,8 +1,5 @@
|
|||||||
package de.tavolio.auth.identityproviders;
|
package de.tavolio.auth.identityproviders;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import de.tavolio.Role;
|
import de.tavolio.Role;
|
||||||
import de.tavolio.auth.utils.JwtUtils;
|
import de.tavolio.auth.utils.JwtUtils;
|
||||||
import de.tavolio.oidc.IssuerService;
|
import de.tavolio.oidc.IssuerService;
|
||||||
@ -70,7 +67,7 @@ public class ClientIdentityProvider implements IdentityProvider<JWTRequest>
|
|||||||
KeypairEntity keypairEntity = keypairRepo.findById(kid);
|
KeypairEntity keypairEntity = keypairRepo.findById(kid);
|
||||||
if (keypairEntity != null)
|
if (keypairEntity != null)
|
||||||
{
|
{
|
||||||
PublicKey publicKey = jwksService.findByKid(keypairEntity).toPublicKey();
|
PublicKey publicKey = jwksService.generate(keypairEntity).toPublicKey();
|
||||||
RealmEntity realm = keypairEntity.getRealm();
|
RealmEntity realm = keypairEntity.getRealm();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import jakarta.inject.Inject;
|
|||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import jakarta.ws.rs.*;
|
import jakarta.ws.rs.*;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import org.apache.commons.lang3.NotImplementedException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@ -83,7 +84,7 @@ public class OidcResource
|
|||||||
@POST
|
@POST
|
||||||
@Path("/token")
|
@Path("/token")
|
||||||
@Transactional
|
@Transactional
|
||||||
public TokenResponse token(@FormParam("grant_type") String grantType, @FormParam("code") String code) throws NoSuchAlgorithmException, InvalidKeySpecException
|
public TokenResponse token(@FormParam("grant_type") String grantType, @FormParam("code") String code, @FormParam("refresh_token") String refreshToken) throws NoSuchAlgorithmException, InvalidKeySpecException
|
||||||
{
|
{
|
||||||
Grant grant = getGrant(grantType);
|
Grant grant = getGrant(grantType);
|
||||||
|
|
||||||
@ -103,6 +104,14 @@ public class OidcResource
|
|||||||
{
|
{
|
||||||
return clientTokenService.getToken(realmKey);
|
return clientTokenService.getToken(realmKey);
|
||||||
}
|
}
|
||||||
|
if (Grant.PASSWORD.equals(grant))
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
if (Grant.REFRESH_TOKEN.equals(grant))
|
||||||
|
{
|
||||||
|
return userTokenService.refreshAccessToken(realmKey, refreshToken);
|
||||||
|
}
|
||||||
throw new RuntimeException();
|
throw new RuntimeException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,6 +125,14 @@ public class OidcResource
|
|||||||
{
|
{
|
||||||
return Grant.CLIENT_CREDENTIALS;
|
return Grant.CLIENT_CREDENTIALS;
|
||||||
}
|
}
|
||||||
|
if ("refresh_token".equals(grantType))
|
||||||
|
{
|
||||||
|
return Grant.REFRESH_TOKEN;
|
||||||
|
}
|
||||||
|
if ("password".equals(grantType))
|
||||||
|
{
|
||||||
|
return Grant.PASSWORD;
|
||||||
|
}
|
||||||
LOG.error("Invalid grant {} provided", grantType);
|
LOG.error("Invalid grant {} provided", grantType);
|
||||||
throw new RuntimeException();
|
throw new RuntimeException();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ public class UserTokenGenerator
|
|||||||
return Jwt.claims()
|
return Jwt.claims()
|
||||||
.upn(upn)
|
.upn(upn)
|
||||||
.audience(clientId)
|
.audience(clientId)
|
||||||
|
.claim("token_purpose", "access_token")
|
||||||
.claim("realm_key", realmKey)
|
.claim("realm_key", realmKey)
|
||||||
.claim("client_id", clientId)
|
.claim("client_id", clientId)
|
||||||
.expiresAt(expiresAt.toInstant())
|
.expiresAt(expiresAt.toInstant())
|
||||||
@ -30,6 +31,7 @@ public class UserTokenGenerator
|
|||||||
{
|
{
|
||||||
return Jwt.claims()
|
return Jwt.claims()
|
||||||
.upn(upn)
|
.upn(upn)
|
||||||
|
.claim("token_purpose", "refresh_token")
|
||||||
.claim("realm_key", realmKey)
|
.claim("realm_key", realmKey)
|
||||||
.claim("client_id", clientId)
|
.claim("client_id", clientId)
|
||||||
.expiresAt(expiresAt.toInstant())
|
.expiresAt(expiresAt.toInstant())
|
||||||
|
|||||||
@ -9,10 +9,14 @@ import de.tavolio.realm.code.CodeRepo;
|
|||||||
import de.tavolio.realm.RealmEntity;
|
import de.tavolio.realm.RealmEntity;
|
||||||
import de.tavolio.realm.RealmService;
|
import de.tavolio.realm.RealmService;
|
||||||
import de.tavolio.realm.key.KeypairEntity;
|
import de.tavolio.realm.key.KeypairEntity;
|
||||||
|
import de.tavolio.verify.JwtVerificationService;
|
||||||
import io.quarkus.security.identity.SecurityIdentity;
|
import io.quarkus.security.identity.SecurityIdentity;
|
||||||
|
import io.smallrye.jwt.auth.principal.DefaultJWTParser;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.ws.rs.*;
|
import jakarta.ws.rs.*;
|
||||||
|
import org.apache.commons.lang3.Strings;
|
||||||
|
import org.eclipse.microprofile.jwt.JsonWebToken;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@ -41,6 +45,9 @@ public class UserTokenService
|
|||||||
@Inject
|
@Inject
|
||||||
RealmService realmService;
|
RealmService realmService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
JwtVerificationService jwtVerificationService;
|
||||||
|
|
||||||
public TokenResponse getToken(String realmKey, String code)
|
public TokenResponse getToken(String realmKey, String code)
|
||||||
{
|
{
|
||||||
return generateUserToken(realmKey, code);
|
return generateUserToken(realmKey, code);
|
||||||
@ -73,4 +80,45 @@ public class UserTokenService
|
|||||||
}
|
}
|
||||||
throw new BadRequestException();
|
throw new BadRequestException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TokenResponse refreshAccessToken(String realmKey, String refreshToken)
|
||||||
|
{
|
||||||
|
String principal = identity.getPrincipal().getName();
|
||||||
|
|
||||||
|
JsonWebToken token = jwtVerificationService.validate(realmKey, refreshToken);
|
||||||
|
|
||||||
|
String clientId = token.getClaim("client_id");
|
||||||
|
assertClientId(clientId, principal);
|
||||||
|
|
||||||
|
String purpose = token.getClaim("token_purpose");
|
||||||
|
assertTokenPurpose(purpose);
|
||||||
|
|
||||||
|
RealmEntity realm = realmService.requireByKey(realmKey);
|
||||||
|
|
||||||
|
KeypairEntity keypair = realm.getKeys().getFirst();
|
||||||
|
String kid = keypair.getId();
|
||||||
|
PrivateKey signingKey = KeypairEntity.toPrivateKey(keypair);
|
||||||
|
ZonedDateTime expiresAt = ZonedDateTime.now().plusSeconds(realm.getLifetime());
|
||||||
|
return new TokenResponse()
|
||||||
|
.setTokenType("Bearer")
|
||||||
|
.setExpiresAt(expiresAt.toEpochSecond())
|
||||||
|
.setAccessToken(userTokenGenerator.generateAccessToken(realm.getKey(), principal, token.getName(), expiresAt, signingKey, kid))
|
||||||
|
.setRefreshToken(refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertClientId(String tokenClientId, String requestingClientId)
|
||||||
|
{
|
||||||
|
if (!Strings.CI.equals(tokenClientId, requestingClientId))
|
||||||
|
{
|
||||||
|
throw new RuntimeException("JWT client id and requesting client id do not match. Deny refresh.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertTokenPurpose(String purpose)
|
||||||
|
{
|
||||||
|
if (!Strings.CI.equals("refresh_token", purpose))
|
||||||
|
{
|
||||||
|
throw new RuntimeException("Provided token is not a refresh token. Please provide a refresh token.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,5 +2,5 @@ package de.tavolio.realm.client;
|
|||||||
|
|
||||||
public enum Grant
|
public enum Grant
|
||||||
{
|
{
|
||||||
CLIENT_CREDENTIALS, AUTHORIZATION_CODE, PASSWORD
|
CLIENT_CREDENTIALS, AUTHORIZATION_CODE, PASSWORD, REFRESH_TOKEN
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,24 +2,19 @@ package de.tavolio.verify;
|
|||||||
|
|
||||||
import de.tavolio.realm.key.KeypairEntity;
|
import de.tavolio.realm.key.KeypairEntity;
|
||||||
import de.tavolio.realm.key.KeypairRepo;
|
import de.tavolio.realm.key.KeypairRepo;
|
||||||
import de.tavolio.realm.RealmEntity;
|
|
||||||
import de.tavolio.verify.jwks.EcPublicKey;
|
import de.tavolio.verify.jwks.EcPublicKey;
|
||||||
import de.tavolio.verify.jwks.JwksKey;
|
import de.tavolio.verify.jwks.JwksKey;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import org.apache.commons.lang3.NotImplementedException;
|
import org.apache.commons.lang3.NotImplementedException;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class JwksService
|
public class JwksService
|
||||||
{
|
{
|
||||||
@Inject
|
@Inject
|
||||||
KeypairRepo keypairRepo;
|
KeypairRepo keypairRepo;
|
||||||
|
|
||||||
public JwksKey findByKid(KeypairEntity keypair)
|
public JwksKey generate(KeypairEntity keypair)
|
||||||
{
|
{
|
||||||
switch (keypair.getType())
|
switch (keypair.getType())
|
||||||
{
|
{
|
||||||
|
|||||||
51
src/main/java/de/tavolio/verify/JwtVerificationService.java
Normal file
51
src/main/java/de/tavolio/verify/JwtVerificationService.java
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package de.tavolio.verify;
|
||||||
|
|
||||||
|
import de.tavolio.auth.utils.JwtUtils;
|
||||||
|
import de.tavolio.oidc.IssuerService;
|
||||||
|
import de.tavolio.realm.key.KeypairEntity;
|
||||||
|
import de.tavolio.realm.key.KeypairRepo;
|
||||||
|
import io.quarkus.security.AuthenticationFailedException;
|
||||||
|
import io.quarkus.security.UnauthorizedException;
|
||||||
|
import io.smallrye.jwt.auth.principal.DefaultJWTParser;
|
||||||
|
import io.smallrye.jwt.auth.principal.JWTAuthContextInfo;
|
||||||
|
import io.smallrye.jwt.auth.principal.ParseException;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import org.eclipse.microprofile.jwt.JsonWebToken;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class JwtVerificationService
|
||||||
|
{
|
||||||
|
@Inject
|
||||||
|
IssuerService issuerService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
JwksService jwksService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
KeypairRepo keypairRepo;
|
||||||
|
|
||||||
|
public JsonWebToken validate(String realmKey, String jwt)
|
||||||
|
{
|
||||||
|
KeypairEntity keypair = keypairRepo.findById(JwtUtils.parseHeader(jwt).getKid());
|
||||||
|
if (keypair != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new DefaultJWTParser(getContextForRealm(realmKey)).verify(jwt, jwksService.generate(keypair).toPublicKey());
|
||||||
|
}
|
||||||
|
catch (ParseException e)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private JWTAuthContextInfo getContextForRealm(String realmKey)
|
||||||
|
{
|
||||||
|
JWTAuthContextInfo info = new JWTAuthContextInfo();
|
||||||
|
info.setIssuedBy(issuerService.getIssuer(realmKey));
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,12 @@
|
|||||||
|
quarkus.jvm-args=--add-opens java.base/java.lang=ALL-UNNAMED
|
||||||
|
|
||||||
quarkus.http.root-path=/api
|
quarkus.http.root-path=/api
|
||||||
quarkus.http.test-port=9089
|
quarkus.http.test-port=9089
|
||||||
quarkus.http.host=0.0.0.0
|
quarkus.http.host=0.0.0.0
|
||||||
%dev.quarkus.http.port=8089
|
%dev.quarkus.http.port=8089
|
||||||
|
|
||||||
|
quarkus.http.access-log.enabled=true
|
||||||
|
|
||||||
quarkus.http.cors.enabled=true
|
quarkus.http.cors.enabled=true
|
||||||
%dev.quarkus.http.cors.origins=/.*/
|
%dev.quarkus.http.cors.origins=/.*/
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,7 @@ realms:
|
|||||||
allowed-grants:
|
allowed-grants:
|
||||||
- AUTHORIZATION_CODE
|
- AUTHORIZATION_CODE
|
||||||
- CLIENT_CREDENTIALS
|
- CLIENT_CREDENTIALS
|
||||||
|
- REFRESH_TOKEN
|
||||||
analytics-backend:
|
analytics-backend:
|
||||||
secret:
|
secret:
|
||||||
bcrypt: $2a$12$1oYS45e/nXP1OeMgdZZAKeEixarRDzbBGZd0xOnEQQMKlOKwVMrX.
|
bcrypt: $2a$12$1oYS45e/nXP1OeMgdZZAKeEixarRDzbBGZd0xOnEQQMKlOKwVMrX.
|
||||||
|
|||||||
@ -29,7 +29,7 @@ create table allowed_grants
|
|||||||
client_id varchar(255) not null
|
client_id varchar(255) not null
|
||||||
constraint fkpuqnyqud9ft1tn18qrcy7457c references client,
|
constraint fkpuqnyqud9ft1tn18qrcy7457c references client,
|
||||||
grant_name varchar(255)
|
grant_name varchar(255)
|
||||||
constraint allowed_grants_grant_name_check check ((grant_name)::text = ANY ((ARRAY ['CLIENT_CREDENTIALS':: character varying, 'AUTHORIZATION_CODE':: character varying, 'PASSWORD':: character varying])::text[])
|
constraint allowed_grants_grant_name_check check ((grant_name)::text = ANY ((ARRAY ['CLIENT_CREDENTIALS':: character varying, 'AUTHORIZATION_CODE':: character varying, 'PASSWORD':: character varying, 'REFRESH_TOKEN':: character varying])::text[])
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user