add refresh token feature.
This commit is contained in:
parent
dd0003a478
commit
b6de1db857
|
@ -15,15 +15,15 @@ import javax.ws.rs.core.HttpHeaders;
|
|||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Path("token")
|
||||
public class TokenEndpoint {
|
||||
|
||||
List<String> supportedGrantTypes = Collections.singletonList("authorization_code");
|
||||
List<String> supportedGrantTypes = Arrays.asList("authorization_code", "refresh_token");
|
||||
|
||||
@Inject
|
||||
private AppDataRepository appDataRepository;
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
package com.baeldung.oauth2.authorization.server.handler;
|
||||
|
||||
import com.baeldung.oauth2.authorization.server.PEMKeyUtils;
|
||||
import com.nimbusds.jose.*;
|
||||
import com.nimbusds.jose.crypto.RSASSASigner;
|
||||
import com.nimbusds.jose.crypto.RSASSAVerifier;
|
||||
import com.nimbusds.jose.jwk.JWK;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
import org.eclipse.microprofile.config.Config;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class AbstractGrantTypeHandler implements AuthorizationGrantTypeHandler {
|
||||
|
||||
//Always RSA 256, but could be parametrized
|
||||
protected JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JWT).build();
|
||||
|
||||
@Inject
|
||||
protected Config config;
|
||||
|
||||
//30 min
|
||||
protected Long expiresInMin = 30L;
|
||||
|
||||
protected JWSVerifier getJWSVerifier() throws Exception {
|
||||
String verificationkey = config.getValue("verificationkey", String.class);
|
||||
String pemEncodedRSAPublicKey = PEMKeyUtils.readKeyAsString(verificationkey);
|
||||
RSAKey rsaPublicKey = (RSAKey) JWK.parseFromPEMEncodedObjects(pemEncodedRSAPublicKey);
|
||||
return new RSASSAVerifier(rsaPublicKey);
|
||||
}
|
||||
|
||||
protected JWSSigner getJwsSigner() throws Exception {
|
||||
String signingkey = config.getValue("signingkey", String.class);
|
||||
String pemEncodedRSAPrivateKey = PEMKeyUtils.readKeyAsString(signingkey);
|
||||
RSAKey rsaKey = (RSAKey) JWK.parseFromPEMEncodedObjects(pemEncodedRSAPrivateKey);
|
||||
return new RSASSASigner(rsaKey.toRSAPrivateKey());
|
||||
}
|
||||
|
||||
protected String getAccessToken(String clientId, String subject, String approvedScope) throws Exception {
|
||||
//4. Signing
|
||||
JWSSigner jwsSigner = getJwsSigner();
|
||||
|
||||
Instant now = Instant.now();
|
||||
//Long expiresInMin = 30L;
|
||||
Date expirationTime = Date.from(now.plus(expiresInMin, ChronoUnit.MINUTES));
|
||||
|
||||
//3. JWT Payload or claims
|
||||
JWTClaimsSet jwtClaims = new JWTClaimsSet.Builder()
|
||||
.issuer("http://localhost:9080")
|
||||
.subject(subject)
|
||||
.claim("upn", subject)
|
||||
.claim("client_id", clientId)
|
||||
.audience("http://localhost:9280")
|
||||
.claim("scope", approvedScope)
|
||||
.claim("groups", Arrays.asList(approvedScope.split(" ")))
|
||||
.expirationTime(expirationTime) // expires in 30 minutes
|
||||
.notBeforeTime(Date.from(now))
|
||||
.issueTime(Date.from(now))
|
||||
.jwtID(UUID.randomUUID().toString())
|
||||
.build();
|
||||
SignedJWT signedJWT = new SignedJWT(jwsHeader, jwtClaims);
|
||||
signedJWT.sign(jwsSigner);
|
||||
return signedJWT.serialize();
|
||||
}
|
||||
|
||||
protected String getRefreshToken(String clientId, String subject, String approvedScope) throws Exception {
|
||||
JWSSigner jwsSigner = getJwsSigner();
|
||||
Instant now = Instant.now();
|
||||
//6.Build refresh token
|
||||
JWTClaimsSet refreshTokenClaims = new JWTClaimsSet.Builder()
|
||||
.subject(subject)
|
||||
.claim("client_id", clientId)
|
||||
.claim("scope", approvedScope)
|
||||
//refresh token for 1 day.
|
||||
.expirationTime(Date.from(now.plus(1, ChronoUnit.DAYS)))
|
||||
.build();
|
||||
SignedJWT signedRefreshToken = new SignedJWT(jwsHeader, refreshTokenClaims);
|
||||
signedRefreshToken.sign(jwsSigner);
|
||||
return signedRefreshToken.serialize();
|
||||
}
|
||||
}
|
|
@ -1,18 +1,7 @@
|
|||
package com.baeldung.oauth2.authorization.server.handler;
|
||||
|
||||
import com.baeldung.oauth2.authorization.server.PEMKeyUtils;
|
||||
import com.baeldung.oauth2.authorization.server.model.AuthorizationCode;
|
||||
import com.nimbusds.jose.JOSEObjectType;
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import com.nimbusds.jose.JWSHeader;
|
||||
import com.nimbusds.jose.crypto.RSASSASigner;
|
||||
import com.nimbusds.jose.jwk.JWK;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
import org.eclipse.microprofile.config.Config;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.json.Json;
|
||||
import javax.json.JsonObject;
|
||||
|
@ -20,22 +9,14 @@ import javax.persistence.EntityManager;
|
|||
import javax.persistence.PersistenceContext;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
@Named("authorization_code")
|
||||
public class AuthorizationCodeGrantTypeHandler implements AuthorizationGrantTypeHandler {
|
||||
public class AuthorizationCodeGrantTypeHandler extends AbstractGrantTypeHandler {
|
||||
|
||||
@PersistenceContext
|
||||
private EntityManager entityManager;
|
||||
|
||||
@Inject
|
||||
private Config config;
|
||||
|
||||
@Override
|
||||
public JsonObject createAccessToken(String clientId, MultivaluedMap<String, String> params) throws Exception {
|
||||
//1. code is required
|
||||
|
@ -58,42 +39,16 @@ public class AuthorizationCodeGrantTypeHandler implements AuthorizationGrantType
|
|||
throw new WebApplicationException("invalid_grant");
|
||||
}
|
||||
|
||||
//JWT Header
|
||||
JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JWT).build();
|
||||
|
||||
Instant now = Instant.now();
|
||||
Long expiresInMin = 30L;
|
||||
Date expiresIn = Date.from(now.plus(expiresInMin, ChronoUnit.MINUTES));
|
||||
|
||||
//3. JWT Payload or claims
|
||||
JWTClaimsSet jwtClaims = new JWTClaimsSet.Builder()
|
||||
.issuer("http://localhost:9080")
|
||||
.subject(authorizationCode.getUserId())
|
||||
.claim("upn", authorizationCode.getUserId())
|
||||
.audience("http://localhost:9280")
|
||||
.claim("scope", authorizationCode.getApprovedScopes())
|
||||
.claim("groups", Arrays.asList(authorizationCode.getApprovedScopes().split(" ")))
|
||||
.expirationTime(expiresIn) // expires in 30 minutes
|
||||
.notBeforeTime(Date.from(now))
|
||||
.issueTime(Date.from(now))
|
||||
.jwtID(UUID.randomUUID().toString())
|
||||
.build();
|
||||
SignedJWT signedJWT = new SignedJWT(jwsHeader, jwtClaims);
|
||||
|
||||
//4. Signing
|
||||
String signingkey = config.getValue("signingkey", String.class);
|
||||
String pemEncodedRSAPrivateKey = PEMKeyUtils.readKeyAsString(signingkey);
|
||||
RSAKey rsaKey = (RSAKey) JWK.parseFromPEMEncodedObjects(pemEncodedRSAPrivateKey);
|
||||
signedJWT.sign(new RSASSASigner(rsaKey.toRSAPrivateKey()));
|
||||
|
||||
//5. Finally the JWT access token
|
||||
String accessToken = signedJWT.serialize();
|
||||
String accessToken = getAccessToken(clientId, authorizationCode.getUserId(), authorizationCode.getApprovedScopes());
|
||||
String refreshToken = getRefreshToken(clientId, authorizationCode.getUserId(), authorizationCode.getApprovedScopes());
|
||||
|
||||
return Json.createObjectBuilder()
|
||||
.add("token_type", "Bearer")
|
||||
.add("access_token", accessToken)
|
||||
.add("expires_in", expiresInMin * 60)
|
||||
.add("scope", authorizationCode.getApprovedScopes())
|
||||
.add("refresh_token", refreshToken)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
package com.baeldung.oauth2.authorization.server.handler;
|
||||
|
||||
import com.nimbusds.jose.JWSVerifier;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.json.Json;
|
||||
import javax.json.JsonObject;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import java.util.*;
|
||||
|
||||
@Named("refresh_token")
|
||||
public class RefreshTokenGrantTypeHandler extends AbstractGrantTypeHandler {
|
||||
|
||||
@Override
|
||||
public JsonObject createAccessToken(String clientId, MultivaluedMap<String, String> params) throws Exception {
|
||||
String refreshToken = params.getFirst("refresh_token");
|
||||
if (refreshToken == null || "".equals(refreshToken)) {
|
||||
throw new WebApplicationException("invalid_grant");
|
||||
}
|
||||
|
||||
//Decode refresh token
|
||||
SignedJWT signedRefreshToken = SignedJWT.parse(refreshToken);
|
||||
JWSVerifier verifier = getJWSVerifier();
|
||||
|
||||
if (!signedRefreshToken.verify(verifier)) {
|
||||
throw new WebApplicationException("Invalid refresh token.");
|
||||
}
|
||||
if (!(new Date().before(signedRefreshToken.getJWTClaimsSet().getExpirationTime()))) {
|
||||
throw new WebApplicationException("Refresh token expired.");
|
||||
}
|
||||
String refreshTokenClientId = signedRefreshToken.getJWTClaimsSet().getStringClaim("client_id");
|
||||
if (!clientId.equals(refreshTokenClientId)) {
|
||||
throw new WebApplicationException("Invalid client_id.");
|
||||
}
|
||||
|
||||
//At this point, the refresh token is valid and not yet expired
|
||||
//So create a new access token from it.
|
||||
String subject = signedRefreshToken.getJWTClaimsSet().getSubject();
|
||||
String approvedScopes = signedRefreshToken.getJWTClaimsSet().getStringClaim("scope");
|
||||
|
||||
String finalScope = approvedScopes;
|
||||
String requestedScopes = params.getFirst("scope");
|
||||
if (requestedScopes != null && !requestedScopes.isEmpty()) {
|
||||
Set<String> allowedScopes = new LinkedHashSet<>();
|
||||
Set<String> rScopes = new HashSet(Arrays.asList(requestedScopes.split(" ")));
|
||||
Set<String> aScopes = new HashSet(Arrays.asList(approvedScopes.split(" ")));
|
||||
for (String scope : rScopes) {
|
||||
if (aScopes.contains(scope)) allowedScopes.add(scope);
|
||||
}
|
||||
finalScope = String.join(" ", allowedScopes);
|
||||
}
|
||||
|
||||
String accessToken = getAccessToken(clientId, subject, finalScope);
|
||||
return Json.createObjectBuilder()
|
||||
.add("token_type", "Bearer")
|
||||
.add("access_token", accessToken)
|
||||
.add("expires_in", expiresInMin * 60)
|
||||
.add("scope", finalScope)
|
||||
.add("refresh_token", refreshToken)
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package com.baeldung.oauth2.client;
|
||||
|
||||
import javax.servlet.RequestDispatcher;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
|
||||
public abstract class AbstractServlet extends HttpServlet {
|
||||
|
||||
protected void dispatch(String location, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
RequestDispatcher requestDispatcher = request.getRequestDispatcher(location);
|
||||
requestDispatcher.forward(request, response);
|
||||
}
|
||||
|
||||
protected String getAuthorizationHeaderValue(String clientId, String clientSecret) {
|
||||
String token = clientId + ":" + clientSecret;
|
||||
String encodedString = Base64.getEncoder().encodeToString(token.getBytes());
|
||||
return "Basic " + encodedString;
|
||||
}
|
||||
}
|
|
@ -29,6 +29,9 @@ public class CallbackServlet extends HttpServlet {
|
|||
@Override
|
||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
|
||||
String clientId = config.getValue("client.clientId", String.class);
|
||||
String clientSecret = config.getValue("client.clientSecret", String.class);
|
||||
|
||||
//Error:
|
||||
String error = request.getParameter("error");
|
||||
if (error != null) {
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
package com.baeldung.oauth2.client;
|
||||
|
||||
import org.eclipse.microprofile.config.Config;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.json.JsonObject;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.annotation.WebServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.client.Client;
|
||||
import javax.ws.rs.client.ClientBuilder;
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.client.WebTarget;
|
||||
import javax.ws.rs.core.Form;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.io.IOException;
|
||||
|
||||
@WebServlet(urlPatterns = "/refreshtoken")
|
||||
public class RefreshTokenServlet extends AbstractServlet {
|
||||
|
||||
@Inject
|
||||
private Config config;
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
|
||||
String clientId = config.getValue("client.clientId", String.class);
|
||||
String clientSecret = config.getValue("client.clientSecret", String.class);
|
||||
|
||||
JsonObject actualTokenResponse = (JsonObject) request.getSession().getAttribute("tokenResponse");
|
||||
Client client = ClientBuilder.newClient();
|
||||
WebTarget target = client.target(config.getValue("provider.tokenUri", String.class));
|
||||
|
||||
Form form = new Form();
|
||||
form.param("grant_type", "refresh_token");
|
||||
form.param("refresh_token", actualTokenResponse.getString("refresh_token"));
|
||||
|
||||
String scope = request.getParameter("scope");
|
||||
if (scope != null && !scope.isEmpty()) {
|
||||
form.param("scope", scope);
|
||||
}
|
||||
|
||||
JsonObject tokenResponse = target.request(MediaType.APPLICATION_JSON_TYPE)
|
||||
.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeaderValue(clientId, clientSecret))
|
||||
.post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE), JsonObject.class);
|
||||
|
||||
request.getSession().setAttribute("tokenResponse", tokenResponse);
|
||||
dispatch("/", request, response);
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@
|
|||
body {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
input[type=text], input[type=password] {
|
||||
width: 75%;
|
||||
padding: 4px 0px;
|
||||
|
@ -17,6 +18,7 @@
|
|||
border: 1px solid #502bcc;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.container-error {
|
||||
padding: 16px;
|
||||
border: 1px solid #cc102a;
|
||||
|
@ -25,6 +27,7 @@
|
|||
margin-left: 25px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 16px;
|
||||
border: 1px solid #130ecc;
|
||||
|
@ -81,8 +84,20 @@
|
|||
<li>access_token: ${tokenResponse.getString("access_token")}</li>
|
||||
<li>scope: ${tokenResponse.getString("scope")}</li>
|
||||
<li>Expires in (s): ${tokenResponse.getInt("expires_in")}</li>
|
||||
<li>refresh_token: ${tokenResponse.getString("refresh_token")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<span><h4>Refresh Token</h4></span>
|
||||
<hr>
|
||||
<ul>
|
||||
<li><a href="refreshtoken">Refresh token (original scope)</a></li>
|
||||
<li><a href="refreshtoken?scope=resource.read">Refresh token (scope: resource.read)</a></li>
|
||||
<li><a href="refreshtoken?scope=resource.write">Refresh token (scope: resource.write)</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<span><h4>OAuth2 Resource Server Call</h4></span>
|
||||
<hr>
|
||||
|
@ -90,7 +105,6 @@
|
|||
<li><a href="downstream?action=read">Read Protected Resource</a></li>
|
||||
<li><a href="downstream?action=write">Write Protected Resource</a></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
|
Loading…
Reference in New Issue