pl.edu.icm.unity.oauth.as.token.RevocationResource Maven / Gradle / Ivy
Show all versions of unity-server-oauth Show documentation
/*
* Copyright (c) 2015, Jirav All rights reserved.
* See LICENCE.txt file for licensing information.
*/
package pl.edu.icm.unity.oauth.as.token;
import java.util.Arrays;
import java.util.Optional;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Response;
import org.apache.logging.log4j.Logger;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.nimbusds.oauth2.sdk.ErrorObject;
import com.nimbusds.oauth2.sdk.OAuth2Error;
import com.nimbusds.oauth2.sdk.client.ClientType;
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
import pl.edu.icm.unity.base.authn.AuthenticationRealm;
import pl.edu.icm.unity.base.entity.EntityParam;
import pl.edu.icm.unity.base.exceptions.EngineException;
import pl.edu.icm.unity.base.exceptions.WrongArgumentException;
import pl.edu.icm.unity.base.token.Token;
import pl.edu.icm.unity.base.utils.Log;
import pl.edu.icm.unity.engine.api.authn.InvocationContext;
import pl.edu.icm.unity.engine.api.authn.LoginSession;
import pl.edu.icm.unity.engine.api.session.SessionManagement;
import pl.edu.icm.unity.oauth.as.OAuthToken;
import pl.edu.icm.unity.oauth.as.token.access.OAuthAccessTokenRepository;
import pl.edu.icm.unity.oauth.as.token.access.OAuthRefreshTokenRepository;
import pl.edu.icm.unity.store.api.TokenDAO.TokenNotFoundException;
/**
* Implementation of RFC 7009 https://tools.ietf.org/html/rfc7009
*
* Limitations:
* The endpoint access is not authorized - or better said the access
* is authorized implicitly by providing a valid access token to be revoked. The client_id must be always given.
*
* Typical usage:
*
* POST /.../revoke HTTP/1.1
Host: ...
Content-Type: application/x-www-form-urlencoded
token=45ghiukldjahdnhzdauz&client_id=oauth-client&token_type_hint=refresh_token
*
*
* Unity also supports one non standard extension. If a logout=true parameter is added, then
* besides token revocation also the token's owner's session is killed. To allow for this,
* a special OAuth scope must be associated with the token: 'single-logout'.
*
*
* @author K. Benedyczak
*/
@Produces("application/json")
@Path(OAuthTokenEndpoint.TOKEN_REVOCATION_PATH)
public class RevocationResource extends BaseOAuthResource
{
private static final Logger log = Log.getLogger(Log.U_SERVER_OAUTH, RevocationResource.class);
public static final String TOKEN_TYPE = "token_type_hint";
public static final String TOKEN_TYPE_ACCESS = "access_token";
public static final String TOKEN_TYPE_REFRESH = "refresh_token";
public static final String UNSUPPORTED_TOKEN_TYPE_ERROR = "unsupported_token_type";
public static final String TOKEN = "token";
public static final String CLIENT = "client_id";
public static final String LOGOUT = "logout";
public static final String LOGOUT_SCOPE = "single-logout";
private final SessionManagement sessionManagement;
private final AuthenticationRealm realm;
private final OAuthAccessTokenRepository accessTokenRepository;
private final boolean allowUnauthenticatedRevocation;
private final OAuthRefreshTokenRepository refreshTokenRepository;
public RevocationResource(OAuthAccessTokenRepository accessTokenRepository,
OAuthRefreshTokenRepository refreshTokenRepository,
SessionManagement sessionManagement,
AuthenticationRealm realm,
boolean allowUnauthenticatedRevocation)
{
this.accessTokenRepository = accessTokenRepository;
this.refreshTokenRepository = refreshTokenRepository;
this.sessionManagement = sessionManagement;
this.realm = realm;
this.allowUnauthenticatedRevocation = allowUnauthenticatedRevocation;
}
@Path("/")
@POST
public Response revoke(@FormParam(TOKEN) String token, @FormParam(CLIENT) String clientId,
@FormParam(TOKEN_TYPE) String tokenHint, @FormParam(LOGOUT) String logout)
throws EngineException, JsonProcessingException
{
if (token == null)
return makeError(OAuth2Error.INVALID_REQUEST, "To access the token revocation endpoint "
+ "a token must be provided");
if (tokenHint != null && !TOKEN_TYPE_ACCESS.equals(tokenHint) && !TOKEN_TYPE_REFRESH.equals(tokenHint))
return makeError(new ErrorObject(UNSUPPORTED_TOKEN_TYPE_ERROR, "Invalid request",
HTTPResponse.SC_BAD_REQUEST),
"Token type '" + tokenHint + "' is not supported");
Token internalToken;
try
{
internalToken = loadToken(token, tokenHint);
} catch (TokenNotFoundException e)
{
return toResponse(Response.ok());
}
OAuthToken parsedToken = parseInternalToken(internalToken);
if (clientId != null && !clientId.equals(parsedToken.getClientUsername()))
return makeError(OAuth2Error.INVALID_CLIENT, "Wrong client/token");
ClientType effectiveClientType = getEffectiveClientType(parsedToken);
if (effectiveClientType == ClientType.PUBLIC)
{
if (clientId == null)
return makeError(OAuth2Error.INVALID_REQUEST, "To access the token revocation endpoint "
+ "a " + CLIENT + " must be provided");
} else
{
InvocationContext invocationContext = InvocationContext.getCurrent();
LoginSession loginSession = invocationContext.getLoginSession();
if (loginSession == null)
{
log.info("Blocking a try to revoke OAuth token owned by confidential client {} wihtout authentication",
parsedToken.getClientId());
return makeError(OAuth2Error.INVALID_REQUEST, "Authentication is required");
}
if (parsedToken.getClientId() != loginSession.getEntityId())
{
log.warn("OAuth client authenticated with id {} tried to revoke token associated with other client {}",
loginSession.getEntityId(), parsedToken.getClientId());
return makeError(OAuth2Error.INVALID_REQUEST, "Authentication error");
}
}
if ("true".equals(logout))
{
Response r = killSession(parsedToken, internalToken.getOwner());
if (r != null)
return r;
}
try
{
removeToken(token, parsedToken, tokenHint, internalToken.getOwner());
} catch (TokenNotFoundException e)
{
//ok
}
return toResponse(Response.ok());
}
private ClientType getEffectiveClientType(OAuthToken parsedToken)
{
if (allowUnauthenticatedRevocation)
return ClientType.PUBLIC;
return parsedToken.getClientType() == null ? ClientType.CONFIDENTIAL : parsedToken.getClientType();
}
private Token loadToken(String token, String tokenHint)
{
if (TOKEN_TYPE_ACCESS.equals(tokenHint))
{
return accessTokenRepository.readAccessToken(token);
} else if (TOKEN_TYPE_REFRESH.equals(tokenHint))
{
return refreshTokenRepository.readRefreshToken(token);
} else
{
try
{
return accessTokenRepository.readAccessToken(token);
} catch (TokenNotFoundException notFound)
{
return refreshTokenRepository.readRefreshToken(token);
}
}
}
private void removeToken(String token, OAuthToken parsedToken, String tokenHint, long userId)
{
if (TOKEN_TYPE_ACCESS.equals(tokenHint))
{
accessTokenRepository.removeAccessToken(token);
} else if (TOKEN_TYPE_REFRESH.equals(tokenHint))
{
refreshTokenRepository.removeRefreshToken(token, parsedToken, userId);
} else
{
try
{
accessTokenRepository.removeAccessToken(token);
} catch (TokenNotFoundException notFound)
{
refreshTokenRepository.removeRefreshToken(token, parsedToken, userId);
}
}
}
private Response killSession(OAuthToken parsedAccessToken, long entity) throws EngineException
{
if (parsedAccessToken.getEffectiveScope() == null)
return makeError(OAuth2Error.INVALID_SCOPE, "Insufficent scope to perform full logout.");
Optional logoutScope = Arrays.stream(parsedAccessToken.getEffectiveScope()).
filter(scope -> LOGOUT_SCOPE.equals(scope)).
findAny();
if (!logoutScope.isPresent())
return makeError(OAuth2Error.INVALID_SCOPE, "Insufficent scope to perform full logout.");
try
{
LoginSession ownedSession = sessionManagement.getOwnedSession(
new EntityParam(entity), realm.getName());
sessionManagement.removeSession(ownedSession.getId(), true);
} catch (WrongArgumentException e)
{
//ok - no session
}
return null;
}
}