blob: f9979d3ec996f4eed6fc1fda0e9f414ba5b58803 [file] [log] [blame]
/*
* Copyright (c) 2013, 2019 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package org.glassfish.jersey.client.oauth2;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.Feature;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.ReaderInterceptor;
import javax.inject.Inject;
import javax.inject.Provider;
import org.glassfish.jersey.client.oauth2.internal.LocalizationMessages;
import org.glassfish.jersey.internal.PropertiesDelegate;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.message.MessageBodyWorkers;
/**
* Default implementation of {@link OAuth2CodeGrantFlow}.
*
* @author Miroslav Fuksa
* @since 2.3
*/
class AuthCodeGrantImpl implements OAuth2CodeGrantFlow {
/**
* Builder implementation.
*/
static class Builder implements OAuth2CodeGrantFlow.Builder {
private String accessTokenUri;
private String refreshTokenUri;
private String authorizationUri;
private String callbackUri;
private ClientIdentifier clientIdentifier;
private Client client;
private String scope;
private Map<String, String> authorizationProperties = new HashMap<>();
private Map<String, String> accessTokenProperties = new HashMap<>();
private Map<String, String> refreshTokenProperties = new HashMap<>();
/**
* Create a new builder.
*/
public Builder() {
}
/**
* Create a new builder with defined URIs and client id.
*/
public Builder(final ClientIdentifier clientIdentifier, final String authorizationUri, final String accessTokenUri) {
this();
this.accessTokenUri = accessTokenUri;
this.authorizationUri = authorizationUri;
this.clientIdentifier = clientIdentifier;
}
/**
* Create a new builder with defined URIs and client id and callback uri.
*/
public Builder(final ClientIdentifier clientIdentifier, final String authorizationUri, final String accessTokenUri,
final String callbackUri) {
this();
this.accessTokenUri = accessTokenUri;
this.authorizationUri = authorizationUri;
this.callbackUri = callbackUri;
this.clientIdentifier = clientIdentifier;
}
@Override
public Builder accessTokenUri(final String accessTokenUri) {
this.accessTokenUri = accessTokenUri;
return this;
}
@Override
public Builder authorizationUri(final String authorizationUri) {
this.authorizationUri = authorizationUri;
return this;
}
@Override
public Builder redirectUri(final String redirectUri) {
this.callbackUri = redirectUri;
return this;
}
@Override
public Builder clientIdentifier(final ClientIdentifier clientIdentifier) {
this.clientIdentifier = clientIdentifier;
return this;
}
@Override
public Builder scope(final String scope) {
this.scope = scope;
return this;
}
@Override
public Builder client(final Client client) {
this.client = client;
return this;
}
@Override
public Builder refreshTokenUri(final String refreshTokenUri) {
this.refreshTokenUri = refreshTokenUri;
return this;
}
@Override
public Builder property(final OAuth2CodeGrantFlow.Phase phase, final String key, final String value) {
phase.property(key, value, authorizationProperties, accessTokenProperties, refreshTokenProperties);
return this;
}
String getAccessTokenUri() {
return accessTokenUri;
}
String getRefreshTokenUri() {
return refreshTokenUri;
}
String getAuthorizationUri() {
return authorizationUri;
}
String getScope() {
return scope;
}
String getCallbackUri() {
return callbackUri;
}
ClientIdentifier getClientIdentifier() {
return clientIdentifier;
}
Client getClient() {
return client;
}
Map<String, String> getAuthorizationProperties() {
return authorizationProperties;
}
Map<String, String> getAccessTokenProperties() {
return accessTokenProperties;
}
Map<String, String> getRefreshTokenProperties() {
return refreshTokenProperties;
}
@Override
public AuthCodeGrantImpl build() {
return new AuthCodeGrantImpl(authorizationUri, accessTokenUri,
callbackUri, refreshTokenUri,
clientIdentifier,
scope, client, authorizationProperties, accessTokenProperties, refreshTokenProperties);
}
}
private AuthCodeGrantImpl(final String authorizationUri, final String accessTokenUri, final String redirectUri,
final String refreshTokenUri,
final ClientIdentifier clientIdentifier,
final String scope, final Client client, final Map<String, String> authorizationProperties,
final Map<String, String> accessTokenProperties,
final Map<String, String> refreshTokenProperties) {
this.accessTokenUri = accessTokenUri;
this.authorizationUri = authorizationUri;
this.authorizationProperties = authorizationProperties;
this.accessTokenProperties = accessTokenProperties;
this.refreshTokenProperties = refreshTokenProperties;
if (refreshTokenUri != null) {
this.refreshTokenUri = refreshTokenUri;
} else {
this.refreshTokenUri = accessTokenUri;
}
this.clientIdentifier = clientIdentifier;
this.client = configureClient(client);
initDefaultProperties(redirectUri, scope);
}
private Client configureClient(Client client) {
if (client == null) {
client = ClientBuilder.newClient();
}
final Configuration config = client.getConfiguration();
if (!config.isRegistered(AuthCodeGrantImpl.DefaultTokenMessageBodyReader.class)) {
client.register(AuthCodeGrantImpl.DefaultTokenMessageBodyReader.class);
}
if (!config.isRegistered(JacksonFeature.class)) {
client.register(JacksonFeature.class);
}
return client;
}
private void setDefaultProperty(final String key, final String value, final Map<String, String>... properties) {
if (value == null) {
return;
}
for (final Map<String, String> props : properties) {
if (props.get(key) == null) {
props.put(key, value);
}
}
}
private void initDefaultProperties(final String redirectUri, final String scope) {
setDefaultProperty(OAuth2Parameters.RESPONSE_TYPE, "code", authorizationProperties);
setDefaultProperty(OAuth2Parameters.CLIENT_ID, clientIdentifier.getClientId(), authorizationProperties,
accessTokenProperties, refreshTokenProperties);
setDefaultProperty(OAuth2Parameters.REDIRECT_URI, redirectUri == null
? OAuth2Parameters.REDIRECT_URI_UNDEFINED : redirectUri, authorizationProperties, accessTokenProperties);
setDefaultProperty(OAuth2Parameters.STATE, UUID.randomUUID().toString(), authorizationProperties);
setDefaultProperty(OAuth2Parameters.SCOPE, scope, authorizationProperties);
setDefaultProperty(OAuth2Parameters.CLIENT_SECRET, clientIdentifier.getClientSecret(), accessTokenProperties,
refreshTokenProperties);
setDefaultProperty(OAuth2Parameters.GrantType.key,
OAuth2Parameters.GrantType.AUTHORIZATION_CODE.name().toLowerCase(Locale.ROOT),
accessTokenProperties);
setDefaultProperty(OAuth2Parameters.GrantType.key,
OAuth2Parameters.GrantType.REFRESH_TOKEN.name().toLowerCase(Locale.ROOT),
refreshTokenProperties);
}
private final String accessTokenUri;
private final String authorizationUri;
private final String refreshTokenUri;
private final ClientIdentifier clientIdentifier;
private final Client client;
private final Map<String, String> authorizationProperties;
private final Map<String, String> accessTokenProperties;
private final Map<String, String> refreshTokenProperties;
private volatile TokenResult tokenResult;
@Override
public String start() {
final UriBuilder uriBuilder = UriBuilder.fromUri(authorizationUri);
for (final Map.Entry<String, String> entry : authorizationProperties.entrySet()) {
uriBuilder.queryParam(entry.getKey(), entry.getValue());
}
return uriBuilder.build().toString();
}
@Override
public TokenResult finish(final String authorizationCode, final String state) {
if (!this.authorizationProperties.get(OAuth2Parameters.STATE).equals(state)) {
throw new IllegalArgumentException(LocalizationMessages.ERROR_FLOW_WRONG_STATE());
}
accessTokenProperties.put(OAuth2Parameters.CODE, authorizationCode);
final Form form = new Form();
for (final Map.Entry<String, String> entry : accessTokenProperties.entrySet()) {
form.param(entry.getKey(), entry.getValue());
}
final Response response = client.target(accessTokenUri)
.request(MediaType.APPLICATION_JSON_TYPE)
.post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE));
if (response.getStatus() != 200) {
throw new ProcessingException(LocalizationMessages.ERROR_FLOW_REQUEST_ACCESS_TOKEN(response.getStatus()));
}
this.tokenResult = response.readEntity(TokenResult.class);
return tokenResult;
}
@Override
public TokenResult refreshAccessToken(final String refreshToken) {
refreshTokenProperties.put(OAuth2Parameters.REFRESH_TOKEN, refreshToken);
final Form form = new Form();
for (final Map.Entry<String, String> entry : refreshTokenProperties.entrySet()) {
form.param(entry.getKey(), entry.getValue());
}
final Response response = client.target(refreshTokenUri)
.request(MediaType.APPLICATION_JSON_TYPE)
.post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE));
if (response.getStatus() != 200) {
throw new ProcessingException(LocalizationMessages.ERROR_FLOW_REQUEST_REFRESH_TOKEN(response.getStatus()));
}
this.tokenResult = response.readEntity(TokenResult.class);
return tokenResult;
}
@Override
public Client getAuthorizedClient() {
return ClientBuilder.newClient().register(getOAuth2Feature());
}
@Override
public Feature getOAuth2Feature() {
if (this.tokenResult == null) {
throw new IllegalStateException(LocalizationMessages.ERROR_FLOW_NOT_FINISHED());
}
return new OAuth2ClientFeature(tokenResult.getAccessToken());
}
static class DefaultTokenMessageBodyReader implements MessageBodyReader<TokenResult> {
// Provider here prevents circular dependency error from HK2 (workers inject providers and this provider inject workers)
@Inject
private Provider<MessageBodyWorkers> workers;
@Inject
private Provider<PropertiesDelegate> propertiesDelegateProvider;
private static Iterable<ReaderInterceptor> EMPTY_INTERCEPTORS = new ArrayList<>();
@Override
public boolean isReadable(final Class<?> type,
final Type genericType,
final Annotation[] annotations,
final MediaType mediaType) {
return type.equals(TokenResult.class);
}
@Override
public TokenResult readFrom(final Class<TokenResult> type, final Type genericType, final Annotation[] annotations,
final MediaType mediaType, final MultivaluedMap<String, String> httpHeaders,
final InputStream entityStream) throws IOException, WebApplicationException {
final GenericType<Map<String, Object>> mapType = new GenericType<Map<String, Object>>() {
};
final Map<String, Object> map = (Map<String, Object>) workers.get().readFrom(mapType.getRawType(),
mapType.getType(), annotations,
mediaType, httpHeaders,
propertiesDelegateProvider.get(),
entityStream, EMPTY_INTERCEPTORS, false);
return new TokenResult(map);
}
}
}