🚧 Add refresh grant
This commit is contained in:
parent
308ec0c93a
commit
1aebd3b50c
@ -1,10 +1,16 @@
|
|||||||
package dev.dinauer.oidcproxy;
|
package dev.dinauer.oidcproxy;
|
||||||
|
|
||||||
|
import dev.dinauer.oidcproxy.session.SessionCache;
|
||||||
|
import dev.dinauer.oidcproxy.session.SessionService;
|
||||||
import io.vertx.core.http.Cookie;
|
import io.vertx.core.http.Cookie;
|
||||||
|
import io.vertx.core.http.HttpServerRequest;
|
||||||
import io.vertx.core.http.HttpServerResponse;
|
import io.vertx.core.http.HttpServerResponse;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class LogoutService
|
public class LogoutService
|
||||||
{
|
{
|
||||||
@ -13,12 +19,33 @@ public class LogoutService
|
|||||||
@ConfigProperty(name = "oidc.proxy.logout.redirect.url")
|
@ConfigProperty(name = "oidc.proxy.logout.redirect.url")
|
||||||
String logoutRedirectUrl;
|
String logoutRedirectUrl;
|
||||||
|
|
||||||
public void logout(HttpServerResponse response)
|
@Inject
|
||||||
|
SessionCache sessionCache;
|
||||||
|
|
||||||
|
public void logout(HttpServerRequest request, HttpServerResponse response)
|
||||||
{
|
{
|
||||||
|
String sessionId = getSessionId(request.cookies());
|
||||||
|
if (sessionId != null)
|
||||||
|
{
|
||||||
|
sessionCache.remove(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
response.addCookie(Cookie.cookie("session", EMPTY).setMaxAge(0).setPath("/").setHttpOnly(true).setSecure(true));
|
response.addCookie(Cookie.cookie("session", EMPTY).setMaxAge(0).setPath("/").setHttpOnly(true).setSecure(true));
|
||||||
response.addCookie(Cookie.cookie("identity", EMPTY).setMaxAge(0).setPath("/").setHttpOnly(false).setSecure(true));
|
response.addCookie(Cookie.cookie("identity", EMPTY).setMaxAge(0).setPath("/").setHttpOnly(false).setSecure(true));
|
||||||
response.setStatusCode(302);
|
response.setStatusCode(302);
|
||||||
response.putHeader("Location", logoutRedirectUrl);
|
response.putHeader("Location", logoutRedirectUrl);
|
||||||
response.send();
|
response.send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getSessionId(Set<Cookie> cookies)
|
||||||
|
{
|
||||||
|
for (Cookie cookie : cookies)
|
||||||
|
{
|
||||||
|
if ("session".equals(cookie.getName()))
|
||||||
|
{
|
||||||
|
return cookie.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,9 +47,9 @@ public class Resource
|
|||||||
|
|
||||||
@Route(path = "/auth/logout", order = 1)
|
@Route(path = "/auth/logout", order = 1)
|
||||||
@Blocking
|
@Blocking
|
||||||
public void logout(@Context HttpServerResponse response)
|
public void logout(@Context RoutingContext context)
|
||||||
{
|
{
|
||||||
logoutService.logout(response);
|
logoutService.logout(context.request(), context.response());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Route(path = "/api/*", order = 2)
|
@Route(path = "/api/*", order = 2)
|
||||||
|
|||||||
@ -30,7 +30,7 @@ public class CallbackService
|
|||||||
{
|
{
|
||||||
String code = request.params().get("code");
|
String code = request.params().get("code");
|
||||||
TokenResponse oidcResponse = client.exchangeAuthorizationCode(code);
|
TokenResponse oidcResponse = client.exchangeAuthorizationCode(code);
|
||||||
String sessionId = sessionCache.add(oidcResponse.accessToken(), oidcResponse.refreshToken(), Instant.ofEpochSecond(oidcResponse.expiresAt()).atZone(ZoneOffset.UTC));
|
String sessionId = sessionCache.add(oidcResponse.accessToken(), oidcResponse.refreshToken());
|
||||||
|
|
||||||
int cookieExpiry = (int) (JwtUtils.extractExpiresAt(oidcResponse.refreshToken()).toEpochSecond() - ZonedDateTime.now().toEpochSecond());
|
int cookieExpiry = (int) (JwtUtils.extractExpiresAt(oidcResponse.refreshToken()).toEpochSecond() - ZonedDateTime.now().toEpochSecond());
|
||||||
response.addCookie(Cookie.cookie("session", sessionId).setHttpOnly(true).setSecure(true).setPath("/").setMaxAge(cookieExpiry));
|
response.addCookie(Cookie.cookie("session", sessionId).setHttpOnly(true).setSecure(true).setPath("/").setMaxAge(cookieExpiry));
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package dev.dinauer.oidcproxy.callback;
|
package dev.dinauer.oidcproxy.callback;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
|
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import dev.dinauer.oidcproxy.callback.model.TokenResponse;
|
import dev.dinauer.oidcproxy.callback.model.TokenResponse;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
@ -47,7 +49,38 @@ public class OidcClient
|
|||||||
}
|
}
|
||||||
throw new RuntimeException();
|
throw new RuntimeException();
|
||||||
}
|
}
|
||||||
catch (IOException | InterruptedException e)
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
catch (InterruptedException e)
|
||||||
|
{
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TokenResponse refreshAccessToken(String refreshToken)
|
||||||
|
{
|
||||||
|
String body = String.format("grant_type=refresh_token&refresh_token=%s", URLEncoder.encode(refreshToken, StandardCharsets.UTF_8));
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.method("POST", HttpRequest.BodyPublishers.ofString(body))
|
||||||
|
.header("Content-Type", APPLICATION_URLENCODED)
|
||||||
|
.header("Authorization", formatAuthHeader(clientId, clientSecret)).uri(URI.create(oidcAuthUrl)).build();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HttpResponse<String> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (response.statusCode() < 400)
|
||||||
|
{
|
||||||
|
return OBJECT_MAPPER.readValue(response.body(), TokenResponse.class);
|
||||||
|
}
|
||||||
|
throw new RuntimeException();
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
catch (InterruptedException e)
|
||||||
{
|
{
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
|||||||
@ -2,6 +2,15 @@ package dev.dinauer.oidcproxy.session;
|
|||||||
|
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
public record AccessToken(ZonedDateTime expiresAt, String token)
|
public class AccessToken extends OidcToken
|
||||||
{
|
{
|
||||||
|
public AccessToken(String token)
|
||||||
|
{
|
||||||
|
super(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccessToken(String token, ZonedDateTime expiresAt)
|
||||||
|
{
|
||||||
|
super(token, expiresAt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
33
src/main/java/dev/dinauer/oidcproxy/session/OidcToken.java
Normal file
33
src/main/java/dev/dinauer/oidcproxy/session/OidcToken.java
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package dev.dinauer.oidcproxy.session;
|
||||||
|
|
||||||
|
import dev.dinauer.oidcproxy.JwtUtils;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
|
public abstract class OidcToken
|
||||||
|
{
|
||||||
|
private final String token;
|
||||||
|
private final ZonedDateTime expiresAt;
|
||||||
|
|
||||||
|
public OidcToken(String token)
|
||||||
|
{
|
||||||
|
this.token = token;
|
||||||
|
this.expiresAt = JwtUtils.extractExpiresAt(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OidcToken(String token, ZonedDateTime expiresAt)
|
||||||
|
{
|
||||||
|
this.token = token;
|
||||||
|
this.expiresAt = expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ZonedDateTime getExpiresAt()
|
||||||
|
{
|
||||||
|
return expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getToken()
|
||||||
|
{
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package dev.dinauer.oidcproxy.session;
|
||||||
|
|
||||||
|
public class RefreshToken extends OidcToken
|
||||||
|
{
|
||||||
|
public RefreshToken(String token)
|
||||||
|
{
|
||||||
|
super(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -56,36 +56,31 @@ public class SessionCache
|
|||||||
}, 0, 30, TimeUnit.SECONDS);
|
}, 0, 30, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String add(String accessToken, String refreshToken, ZonedDateTime expiresAt)
|
public String add(String accessToken, String refreshToken)
|
||||||
{
|
{
|
||||||
String sessionId = UUID.randomUUID().toString();
|
String sessionId = UUID.randomUUID().toString();
|
||||||
sessionService.create(sessionId, expiresAt, accessToken, refreshToken);
|
sessionService.create(toHash(sessionId), new AccessToken(accessToken), new RefreshToken(refreshToken));
|
||||||
return sessionId;
|
return sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String get(String sessionId) throws TokenNotFoundException
|
public String get(String sessionId) throws TokenNotFoundException
|
||||||
{
|
{
|
||||||
String session = toHash(sessionId);
|
String sessionHash = toHash(sessionId);
|
||||||
AccessToken token = tokens.get(session);
|
AccessToken token = tokens.get(sessionHash);
|
||||||
if (token != null)
|
if (token != null)
|
||||||
{
|
{
|
||||||
return token.token();
|
return token.getToken();
|
||||||
}
|
}
|
||||||
AccessTokenEntity accessTokenEntity = accessTokenRepository.findById(session);
|
AccessToken fromDB = sessionService.provide(sessionHash);
|
||||||
if (session != null)
|
tokens.put(sessionHash, fromDB);
|
||||||
|
return fromDB.getToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void remove(String sessionId)
|
||||||
{
|
{
|
||||||
try
|
String sessionHash = toHash(sessionId);
|
||||||
{
|
tokens.remove(sessionHash);
|
||||||
String accessToken = encryptUtils.decrypt(accessTokenEntity.getToken());
|
sessionService.remove(sessionHash);
|
||||||
tokens.put(session, new AccessToken(accessTokenEntity.getExpiresAt(), accessToken));
|
|
||||||
return accessToken;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new TokenNotFoundException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String toHash(String sessionId)
|
private String toHash(String sessionId)
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
package dev.dinauer.oidcproxy.session;
|
package dev.dinauer.oidcproxy.session;
|
||||||
|
|
||||||
|
import dev.dinauer.oidcproxy.JwtUtils;
|
||||||
|
import dev.dinauer.oidcproxy.callback.OidcClient;
|
||||||
|
import dev.dinauer.oidcproxy.callback.model.TokenResponse;
|
||||||
|
import dev.dinauer.oidcproxy.proxy.exception.TokenNotFoundException;
|
||||||
|
import io.quarkus.security.UnauthorizedException;
|
||||||
import io.smallrye.jwt.auth.principal.DefaultJWTParser;
|
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;
|
||||||
@ -8,6 +13,7 @@ import org.eclipse.microprofile.jwt.JsonWebToken;
|
|||||||
|
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.sql.Ref;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
@ -25,16 +31,16 @@ public class SessionService
|
|||||||
@Inject
|
@Inject
|
||||||
RefreshTokenRepository refreshTokenRepository;
|
RefreshTokenRepository refreshTokenRepository;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
OidcClient client;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void create(String sessionId, ZonedDateTime sessionExpiresAt, String accessToken, String refreshToken)
|
public void create(String sessionHash, AccessToken accessToken, RefreshToken refreshToken)
|
||||||
{
|
{
|
||||||
String sessionHash = toHash(sessionId);
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
JsonWebToken token = new DefaultJWTParser().parseOnly(accessToken);
|
accessTokenRepository.persist(new AccessTokenEntity().setId(sessionHash).setToken(encryptUtils.encrypt(accessToken.getToken())).setExpiresAt(accessToken.getExpiresAt()));
|
||||||
ZonedDateTime accessTokenExpiresAt = Instant.ofEpochSecond(token.getExpirationTime()).atZone(ZoneOffset.UTC);
|
refreshTokenRepository.persist(new RefreshTokenEntity().setId(sessionHash).setToken(encryptUtils.encrypt(refreshToken.getToken())).setExpiresAt(refreshToken.getExpiresAt()));
|
||||||
accessTokenRepository.persist(new AccessTokenEntity().setId(sessionHash).setToken(encryptUtils.encrypt(accessToken)).setExpiresAt(accessTokenExpiresAt));
|
|
||||||
refreshTokenRepository.persist(new RefreshTokenEntity().setId(sessionHash).setToken(encryptUtils.encrypt(refreshToken)).setExpiresAt(sessionExpiresAt));
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@ -42,15 +48,59 @@ public class SessionService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String toHash(String sessionId)
|
@Transactional
|
||||||
|
public AccessToken provide(String sessionHash) throws TokenNotFoundException
|
||||||
|
{
|
||||||
|
AccessTokenEntity dbToken = accessTokenRepository.findById(sessionHash);
|
||||||
|
if (isValid(dbToken))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return Base64.getEncoder().encodeToString( MessageDigest.getInstance("SHA-256").digest(sessionId.getBytes()));
|
return new AccessToken(encryptUtils.decrypt(dbToken.getToken()), dbToken.getExpiresAt());
|
||||||
}
|
}
|
||||||
catch (NoSuchAlgorithmException e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
RefreshTokenEntity dbRefreshToken = refreshTokenRepository.findById(sessionHash);
|
||||||
|
if (dbRefreshToken != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TokenResponse tokenResponse = client.refreshAccessToken(encryptUtils.decrypt(dbRefreshToken.getToken()));
|
||||||
|
renewAccessToken(sessionHash, new AccessToken(tokenResponse.accessToken()));
|
||||||
|
return new AccessToken(tokenResponse.accessToken());
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new TokenNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
throw new TokenNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void remove(String sessionHash)
|
||||||
|
{
|
||||||
|
accessTokenRepository.deleteById(sessionHash);
|
||||||
|
refreshTokenRepository.deleteById(sessionHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValid(AccessTokenEntity token)
|
||||||
|
{
|
||||||
|
if (token != null)
|
||||||
|
{
|
||||||
|
return ZonedDateTime.now().isBefore(token.getExpiresAt());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renewAccessToken(String sessionHash, AccessToken token) throws Exception
|
||||||
|
{
|
||||||
|
accessTokenRepository.deleteById(sessionHash);
|
||||||
|
accessTokenRepository.persist(new AccessTokenEntity().setId(sessionHash).setToken(encryptUtils.encrypt(token.getToken())).setExpiresAt(token.getExpiresAt()));
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,6 @@ oidc.proxy.client.redirect=http://localhost:3000
|
|||||||
|
|
||||||
%dev,test.oidc.proxy.logout.redirect.url=http://localhost:3000
|
%dev,test.oidc.proxy.logout.redirect.url=http://localhost:3000
|
||||||
|
|
||||||
%dev,test.quarkus.flyway.clean-at-start=true
|
%dev,test.quarkus.flyway.clean-at-start=false
|
||||||
%dev.quarkus.flyway.locations=db/migration,db/dev
|
%dev.quarkus.flyway.locations=db/migration,db/dev
|
||||||
quarkus.flyway.migrate-at-start=true
|
quarkus.flyway.migrate-at-start=true
|
||||||
Loading…
x
Reference in New Issue
Block a user