Skip to content

Commit

Permalink
feat(jans-auth-server): align JWT Response for OAuth Token Introspect…
Browse files Browse the repository at this point in the history
…ion with spec #3240 (#4151)

* feat(jans-auth-server): align JWT Response for OAuth Token Introspection with spec #3240

* feat(jans-auth-server): added test for jwt introspection response payload #3240

* doc(jans-auth-server): added doc for jwt introspection response #3240
  • Loading branch information
yuriyz authored Mar 17, 2023
1 parent d81432d commit 02e1595
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 53 deletions.
56 changes: 56 additions & 0 deletions docs/admin/auth-server/endpoints/introspection.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,62 @@ curl -X 'POST' \

## Response as JWT


Response is returned as JWT if `Accept` header has value `application/token-introspection+jwt` or otherwise if explicit endpoint parameter `response_as_jwt` is set to `true`.

```
POST /introspect HTTP/1.1
Host: as.example.com
Accept: application/token-introspection+jwt
```

Sample decoded JWT payload
```json
{
"iss":"https://as.example.com/",
"aud":"https://rs.example.com/resource",
"iat":1514797892,
"token_introspection":
{
"active":true,
"iss":"https://as.example.com/",
"aud":"https://rs.example.com/resource",
"iat":1514797822,
"exp":1514797942,
"client_id":"paiB2goo0a",
"scope":"read write dolphin",
"sub":"Z5O3upPC88QrAjx00dis",
"birthdate":"1982-02-01",
"given_name":"John",
"family_name":"Doe",
"jti":"t1FoCCaZd4Xv4ORJUWVUeTZfsKhW30CQCrWDDjwXy6w"
}
}
```

Sample response (line breaks in payload is for convenience)

```
HTTP/1.1 200 OK
Content-Type: application/token-introspection+jwt
eyJraWQiOiJ3RzZEIiwidHlwIjoidG9rZW4taW50cm9zcGVjdGlvbitqd3QiLCJhbGc
iOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FzLmV4YW1wbGUuY29tLyIsImF1ZCI6I
mh0dHBzOi8vcnMuZXhhbXBsZS5jb20vcmVzb3VyY2UiLCJpYXQiOjE1MTQ3OTc4OTIs
InRva2VuX2ludHJvc3BlY3Rpb24iOnsiYWN0aXZlIjp0cnVlLCJpc3MiOiJodHRwczo
vL2FzLmV4YW1wbGUuY29tLyIsImF1ZCI6Imh0dHBzOi8vcnMuZXhhbXBsZS5jb20vcm
Vzb3VyY2UiLCJpYXQiOjE1MTQ3OTc4MjIsImV4cCI6MTUxNDc5Nzk0MiwiY2xpZW50X
2lkIjoicGFpQjJnb28wYSIsInNjb3BlIjoicmVhZCB3cml0ZSBkb2xwaGluIiwic3Vi
IjoiWjVPM3VwUEM4OFFyQWp4MDBkaXMiLCJiaXJ0aGRhdGUiOiIxOTgyLTAyLTAxIiw
iZ2l2ZW5fbmFtZSI6IkpvaG4iLCJmYW1pbHlfbmFtZSI6IkRvZSIsImp0aSI6InQxRm
9DQ2FaZDRYdjRPUkpVV1ZVZVRaZnNLaFczMENRQ3JXRERqd1h5NncifX0.przJMU5Gh
mNzvwtt1Sr-xa9xTkpiAg5IshbQsRiRVP_7eGR1GHYrNwQh84kxOkHCyje2g5WSRcYo
sGEVIiC-eoPJJ-qBwqwSlgx9JEeCDw2W5DjrblOI_N0Jvsq_dUeOyoWVMqlOydOBhKN
Y0smBrI4NZvEExucOm9WUJXMuJtvq1gBes-0go5j4TEv9sOP9uu81gqWTr_LOo6pgT0
tFFyZfWC4kbXPXiQ2YT6mxCiQRRNM-l9cBdF6Jx6IOrsfFhBuYdYQ_mlL19HgDDOFal
eyqmru6lKlASOsaE8dmLSeKcX91FbG79FKN8un24iwIDCbKT9xlUFl54xWVShNDFA
```

## Disabling The Endpoint Using Feature Flag

`/introspection` endpoint can be enabled or disable using [END_SESSION feature flag](../../reference/json/feature-flags/janssenauthserver-feature-flags.md#introspection).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ private Constants() {
public static final String SALT_FILE_NAME = "salt";
public static final String CERTS_DIR = "certsDir";

public static final String APPLICATION_TOKEN_INTROSPECTION_JWT = "application/token-introspection+jwt";

public static final String ERROR = "error";
public static final String ERROR_DESCRIPTION = "error_description";
public static final String ERROR_URI = "error_uri";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ public void setIssuedAt(Date issuedAt) {
setClaim(ISSUED_AT, issuedAt);
}

public void setIatNow() {
setIssuedAt(new Date());
}

/**
* Identifies the principal that issued the JWT.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,31 @@
package io.jans.as.server.introspection.ws.rs;

import com.google.common.collect.Lists;
import io.jans.as.common.claims.Audience;
import io.jans.as.common.service.AttributeService;
import io.jans.as.model.authorize.AuthorizeErrorResponseType;
import io.jans.as.model.common.IntrospectionResponse;
import io.jans.as.model.config.WebKeysConfiguration;
import io.jans.as.model.configuration.AppConfiguration;
import io.jans.as.model.error.ErrorResponseFactory;
import io.jans.as.model.jwt.Jwt;
import io.jans.as.model.uma.UmaScopeType;
import io.jans.as.model.util.Util;
import io.jans.as.server.model.common.AbstractToken;
import io.jans.as.server.model.common.AccessToken;
import io.jans.as.server.model.common.AuthorizationGrant;
import io.jans.as.server.model.common.AuthorizationGrantList;
import io.jans.as.server.model.token.JwtSigner;
import io.jans.as.server.service.ClientService;
import io.jans.as.server.service.IntrospectionService;
import io.jans.as.server.service.external.ExternalIntrospectionService;
import io.jans.as.server.service.external.context.ExternalIntrospectionContext;
import io.jans.as.server.service.token.TokenService;
import io.jans.as.server.util.ServerUtil;
import io.jans.util.Pair;
import jakarta.inject.Inject;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.BooleanUtils;
Expand All @@ -36,26 +40,11 @@
import org.json.JSONObject;
import org.slf4j.Logger;

import jakarta.inject.Inject;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Iterator;

import static io.jans.as.model.util.Util.escapeLog;
import static org.apache.commons.lang.BooleanUtils.isTrue;
Expand Down Expand Up @@ -85,30 +74,33 @@ public class IntrospectionWebService {
private ExternalIntrospectionService externalIntrospectionService;
@Inject
private AttributeService attributeService;

@Inject
private WebKeysConfiguration webKeysConfiguration;
private IntrospectionService introspectionService;

@GET
@Produces(MediaType.APPLICATION_JSON)
public Response introspectGet(@HeaderParam("Authorization") String authorization,
@HeaderParam("Accept") String accept,
@QueryParam("token") String token,
@QueryParam("token_type_hint") String tokenTypeHint,
@QueryParam("response_as_jwt") String responseAsJwt,
@Context HttpServletRequest httpRequest,
@Context HttpServletResponse httpResponse
) {
return introspect(authorization, token, tokenTypeHint, responseAsJwt, httpRequest, httpResponse);
return introspect(authorization, accept, token, tokenTypeHint, responseAsJwt, httpRequest, httpResponse);
}

@POST
@Produces(MediaType.APPLICATION_JSON)
public Response introspectPost(@HeaderParam("Authorization") String authorization,
@HeaderParam("Accept") String accept,
@FormParam("token") String token,
@FormParam("token_type_hint") String tokenTypeHint,
@FormParam("response_as_jwt") String responseAsJwt,
@Context HttpServletRequest httpRequest,
@Context HttpServletResponse httpResponse) {
return introspect(authorization, token, tokenTypeHint, responseAsJwt, httpRequest, httpResponse);
return introspect(authorization, accept, token, tokenTypeHint, responseAsJwt, httpRequest, httpResponse);
}

private AuthorizationGrant validateAuthorization(String authorization, String token) throws UnsupportedEncodingException {
Expand Down Expand Up @@ -146,10 +138,10 @@ private AuthorizationGrant validateAuthorization(String authorization, String to
return authorizationGrant;
}

private Response introspect(String authorization, String token, String tokenTypeHint, String responseAsJwt, HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
private Response introspect(String authorization, String accept, String token, String tokenTypeHint, String responseAsJwt, HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
try {
if (log.isTraceEnabled()) {
log.trace("Introspect token, authorization: {}, token to introspect: {}, tokenTypeHint: {}", escapeLog(authorization), escapeLog(token), escapeLog(tokenTypeHint));
log.trace("Introspect token, authorization: {}, token to introspect: {}, tokenTypeHint: {}, accept: {}", escapeLog(authorization), escapeLog(token), escapeLog(tokenTypeHint), escapeLog(accept));
}

AuthorizationGrant authorizationGrant = validateAuthorization(authorization, token);
Expand Down Expand Up @@ -180,8 +172,8 @@ private Response introspect(String authorization, String token, String tokenType
String scopes = StringUtils.join(response.getScope().toArray(), " ");
responseAsJsonObject.put("scope", scopes);
}
if (Boolean.TRUE.toString().equalsIgnoreCase(responseAsJwt)) {
return Response.status(Response.Status.OK).entity(createResponseAsJwt(responseAsJsonObject, grantOfIntrospectionToken)).build();
if (introspectionService.isJwtResponse(responseAsJwt, accept)) {
return Response.status(Response.Status.OK).entity(introspectionService.createResponseAsJwt(responseAsJsonObject, grantOfIntrospectionToken)).build();
}

return Response.status(Response.Status.OK).entity(responseAsJsonObject.toString()).type(MediaType.APPLICATION_JSON_TYPE).build();
Expand Down Expand Up @@ -234,30 +226,6 @@ private AbstractToken fillResponse(String token, IntrospectionResponse response,
return tokenToIntrospect;
}

private String createResponseAsJwt(JSONObject response, AuthorizationGrant grant) throws Exception {
final JwtSigner jwtSigner = JwtSigner.newJwtSigner(appConfiguration, webKeysConfiguration, grant.getClient());
final Jwt jwt = jwtSigner.newJwt();
Audience.setAudience(jwt.getClaims(), grant.getClient());

Iterator<String> keysIter = response.keys();
while (keysIter.hasNext()) {
String key = keysIter.next();
Object value = response.opt(key);
if (value != null) {
try {
jwt.getClaims().setClaimObject(key, value, false);
} catch (Exception e) {
log.error("Failed to put claims into jwt. Key: " + key + ", response: " + response.toString(), e);
}
}
}
if (log.isTraceEnabled()) {
log.trace("Response before signing: {}", jwt.getClaims().toJsonString());
}

return jwtSigner.sign().toString();
}

private static JSONObject createResponseAsJsonObject(IntrospectionResponse response, AbstractToken tokenToIntrospect) throws JSONException, IOException {
final JSONObject result = new JSONObject(ServerUtil.asJson(response));
if (tokenToIntrospect != null && StringUtils.isNotBlank(tokenToIntrospect.getX5ts256())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,20 @@ public JwtSigner(AppConfiguration appConfiguration, JSONWebKeySet webKeys, Signa
}

public static JwtSigner newJwtSigner(AppConfiguration appConfiguration, JSONWebKeySet webKeys, Client client) throws Exception {
Preconditions.checkNotNull(client);
Preconditions.checkNotNull(client, "Client must not be null");
ClientService clientService = CdiUtil.bean(ClientService.class);
return newJwtSigner(appConfiguration, webKeys, client, clientService.decryptSecret(client.getClientSecret()));
}

public static JwtSigner newJwtSigner(AppConfiguration appConfiguration, JSONWebKeySet webKeys, Client client, String decryptedSecret) {
Preconditions.checkNotNull(client, "Client must not be null");

SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.fromString(appConfiguration.getDefaultSignatureAlgorithm());
if (client.getIdTokenSignedResponseAlg() != null) {
signatureAlgorithm = SignatureAlgorithm.fromString(client.getIdTokenSignedResponseAlg());
}

ClientService clientService = CdiUtil.bean(ClientService.class);
return new JwtSigner(appConfiguration, webKeys, signatureAlgorithm, client.getClientId(), clientService.decryptSecret(client.getClientSecret()));
return new JwtSigner(appConfiguration, webKeys, signatureAlgorithm, client.getClientId(), decryptedSecret);
}

public Jwt newJwt() throws Exception {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package io.jans.as.server.service;

import io.jans.as.common.claims.Audience;
import io.jans.as.common.model.registration.Client;
import io.jans.as.model.config.Constants;
import io.jans.as.model.config.WebKeysConfiguration;
import io.jans.as.model.configuration.AppConfiguration;
import io.jans.as.model.exception.InvalidJwtException;
import io.jans.as.model.jwt.Jwt;
import io.jans.as.server.model.common.AuthorizationGrant;
import io.jans.as.server.model.token.JwtSigner;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.json.JSONObject;
import org.slf4j.Logger;

/**
* @author Yuriy Z
*/

@Named
public class IntrospectionService {

@Inject
private AppConfiguration appConfiguration;

@Inject
private Logger log;

@Inject
private WebKeysConfiguration webKeysConfiguration;

@Inject
private ClientService clientService;

public boolean isJwtResponse(String responseAsJwt, String acceptHeader) {
return Boolean.TRUE.toString().equalsIgnoreCase(responseAsJwt) ||
Constants.APPLICATION_TOKEN_INTROSPECTION_JWT.equalsIgnoreCase(acceptHeader);
}

public JwtSigner createResponseJwt(JSONObject response, AuthorizationGrant grant) throws Exception {
final Client client = grant.getClient();
final JwtSigner jwtSigner = JwtSigner.newJwtSigner(appConfiguration, webKeysConfiguration, client, clientService.decryptSecret(client.getClientSecret()));
final Jwt jwt = jwtSigner.newJwt();
fillPayload(jwt, response, grant);
return jwtSigner;
}

public void fillPayload(Jwt jwt, JSONObject response, AuthorizationGrant grant) throws InvalidJwtException {
final Client client = grant.getClient();
Audience.setAudience(jwt.getClaims(), client);
jwt.getClaims().setIssuer(appConfiguration.getIssuer());
jwt.getClaims().setIatNow();

try {
jwt.getClaims().setClaim("token_introspection", response);
} catch (Exception e) {
log.error("Failed to put claims into jwt. Key: token_introspection, response: " + response.toString(), e);
}

if (log.isTraceEnabled()) {
log.trace("Response before signing: {}", jwt.getClaims().toJsonString());
}
}

public String createResponseAsJwt(JSONObject response, AuthorizationGrant grant) throws Exception {
return createResponseJwt(response, grant).sign().toString();
}
}
Loading

0 comments on commit 02e1595

Please sign in to comment.