add authorizations to retrofit client

added basic auth, api key and oauth support to the service generator using okhttp interceptors

Fix #962
This commit is contained in:
cbornet
2015-08-29 18:14:33 +02:00
parent 28579cee03
commit 265de9654b
24 changed files with 1629 additions and 106 deletions

View File

@@ -52,6 +52,16 @@ public class RetrofitClientCodegen extends DefaultCodegen implements CodegenConf
supportingFiles.add(new SupportingFile("pom.mustache", "", "pom.xml"));
supportingFiles.add(new SupportingFile("service.mustache",
(sourceFolder + File.separator + invokerPackage).replace(".", java.io.File.separator), "ServiceGenerator.java"));
supportingFiles.add(new SupportingFile("auth/basic.mustache",
(sourceFolder + File.separator + invokerPackage + File.separator + "auth").replace(".", java.io.File.separator), "BasicAuthorization.java"));
supportingFiles.add(new SupportingFile("auth/apikey.mustache",
(sourceFolder + File.separator + invokerPackage + File.separator + "auth").replace(".", java.io.File.separator), "ApiKeyAuthorization.java"));
supportingFiles.add(new SupportingFile("auth/oauth.mustache",
(sourceFolder + File.separator + invokerPackage + File.separator + "auth").replace(".", java.io.File.separator), "OauthAuthorization.java"));
supportingFiles.add(new SupportingFile("auth/oauthflow.mustache",
(sourceFolder + File.separator + invokerPackage + File.separator + "auth").replace(".", java.io.File.separator), "OauthFlow.java"));
supportingFiles.add(new SupportingFile("auth/oauthokclient.mustache",
(sourceFolder + File.separator + invokerPackage + File.separator + "auth").replace(".", java.io.File.separator), "OauthOkHttpClient.java"));
languageSpecificPrimitives = new HashSet<String>(
Arrays.asList(

View File

@@ -2,6 +2,7 @@ package {{package}};
import {{modelPackage}}.*;
import retrofit.Callback;
import retrofit.http.*;
import retrofit.mime.*;
import java.util.*;
@@ -14,6 +15,7 @@ public interface {{classname}} {
{{#operation}}
/**
* {{summary}}
* Sync method
* {{notes}}
{{#allParams}} * @param {{paramName}} {{description}}
{{/allParams}} * @return {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}}
@@ -22,8 +24,22 @@ public interface {{classname}} {
{{#isMultipart}}@Multipart{{/isMultipart}}{{^isMultipart}}@FormUrlEncoded{{/isMultipart}}{{/-first}}{{/formParams}}
@{{httpMethod}}("{{path}}")
{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Object{{/returnType}} {{nickname}}({{^allParams}});{{/allParams}}
{{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{#hasMore}}, {{/hasMore}}{{^hasMore}}
);{{/hasMore}}{{/allParams}}
{{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{#hasMore}},{{/hasMore}}{{^hasMore}}
);{{/hasMore}}{{/allParams}}
/**
* {{summary}}
* Async method
{{#allParams}} * @param {{paramName}} {{description}}
{{/allParams}} * @param cb callback method
* @return void
*/
{{#formParams}}{{#-first}}
{{#isMultipart}}@Multipart{{/isMultipart}}{{^isMultipart}}@FormUrlEncoded{{/isMultipart}}{{/-first}}{{/formParams}}
@{{httpMethod}}("{{path}}")
void {{nickname}}(
{{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}},{{/allParams}} Callback<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Object{{/returnType}}> cb
);
{{/operation}}
}
{{/operations}}
{{/operations}}

View File

@@ -0,0 +1,68 @@
package {{invokerPackage}}.auth;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import com.squareup.okhttp.Interceptor;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
public class ApiKeyAuthorization implements Interceptor {
private final String location;
private final String paramName;
private String apiKey;
public ApiKeyAuthorization(String location, String paramName) {
this.location = location;
this.paramName = paramName;
}
public String getLocation() {
return location;
}
public String getParamName() {
return paramName;
}
public String getApiKey() {
return apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
@Override
public Response intercept(Chain chain) throws IOException {
String paramValue;
Request request = chain.request();
if (location == "query") {
String newQuery = request.uri().getQuery();
paramValue = paramName + "=" + apiKey;
if (newQuery == null) {
newQuery = paramValue;
} else {
newQuery += "&" + paramValue;
}
URI newUri;
try {
newUri = new URI(request.uri().getScheme(), request.uri().getAuthority(),
request.uri().getPath(), newQuery, request.uri().getFragment());
} catch (URISyntaxException e) {
throw new IOException(e);
}
request = request.newBuilder().url(newUri.toURL()).build();
} else if (location == "header") {
request = request.newBuilder()
.addHeader(paramName, apiKey)
.build();
}
return chain.proceed(request);
}
}

View File

@@ -0,0 +1,49 @@
package {{invokerPackage}}.auth;
import java.io.IOException;
import com.squareup.okhttp.Credentials;
import com.squareup.okhttp.Interceptor;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
public class BasicAuthorization implements Interceptor {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public void setCredentials(String username, String password) {
this.username = username;
this.password = password;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
// If the request already have an authorization (eg. Basic auth), do nothing
if (request.header("Authorization") == null) {
String credentials = Credentials.basic(username, password);
request = request.newBuilder()
.addHeader("Authorization", credentials)
.build();
}
return chain.proceed(request);
}
}

View File

@@ -0,0 +1,148 @@
package {{invokerPackage}}.auth;
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
import java.io.IOException;
import java.util.Map;
import org.apache.oltu.oauth2.client.OAuthClient;
import org.apache.oltu.oauth2.client.request.OAuthBearerClientRequest;
import org.apache.oltu.oauth2.client.request.OAuthClientRequest;
import org.apache.oltu.oauth2.client.request.OAuthClientRequest.AuthenticationRequestBuilder;
import org.apache.oltu.oauth2.client.request.OAuthClientRequest.TokenRequestBuilder;
import org.apache.oltu.oauth2.client.response.OAuthJSONAccessTokenResponse;
import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
import org.apache.oltu.oauth2.common.message.types.GrantType;
import com.squareup.okhttp.Interceptor;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Request.Builder;
import com.squareup.okhttp.Response;
public class OauthAuthorization implements Interceptor {
private volatile String accessToken;
private OAuthClient oauthClient;
private TokenRequestBuilder tokenRequestBuilder;
private AuthenticationRequestBuilder authenticationRequestBuilder;
public OauthAuthorization( OkHttpClient client, TokenRequestBuilder requestBuilder ) {
this.oauthClient = new OAuthClient(new OauthOkHttpClient(client));
this.tokenRequestBuilder = requestBuilder;
}
public OauthAuthorization(TokenRequestBuilder requestBuilder ) {
this(new OkHttpClient(), requestBuilder);
}
public OauthAuthorization(OauthFlow flow, String authorizationUrl, String tokenUrl, String scopes) {
this(OAuthClientRequest.tokenLocation(tokenUrl).setScope(scopes));
setFlow(flow);
authenticationRequestBuilder = OAuthClientRequest.authorizationLocation(authorizationUrl);
}
public void setFlow(OauthFlow flow) {
switch(flow) {
case accessCode:
case implicit:
tokenRequestBuilder.setGrantType(GrantType.AUTHORIZATION_CODE);
break;
case password:
tokenRequestBuilder.setGrantType(GrantType.PASSWORD);
break;
case application:
tokenRequestBuilder.setGrantType(GrantType.CLIENT_CREDENTIALS);
break;
default:
break;
}
}
@Override
public Response intercept(Chain chain)
throws IOException {
Request request = chain.request();
// If the request already have an authorization (eg. Basic auth), do nothing
if (request.header("Authorization") != null) {
return chain.proceed(request);
}
// If first time, get the token
OAuthClientRequest oAuthRequest;
if (getAccessToken() == null) {
updateAccessToken(null);
}
// Build the request
Builder rb = request.newBuilder();
String requestAccessToken = new String(getAccessToken());
try {
oAuthRequest = new OAuthBearerClientRequest(request.urlString())
.setAccessToken(requestAccessToken)
.buildHeaderMessage();
} catch (OAuthSystemException e) {
throw new IOException(e);
}
for ( Map.Entry<String, String> header : oAuthRequest.getHeaders().entrySet() ) {
rb.addHeader(header.getKey(), header.getValue());
}
rb.url( oAuthRequest.getLocationUri());
//Execute the request
Response response = chain.proceed(rb.build());
// 401 most likely indicates that access token has expired.
// Time to refresh and resend the request
if ( response.code() == HTTP_UNAUTHORIZED ) {
updateAccessToken(requestAccessToken);
return intercept( chain );
}
return response;
}
public synchronized void updateAccessToken(String requestAccessToken) throws IOException {
if (getAccessToken() == null || getAccessToken().equals(requestAccessToken)) {
try {
OAuthJSONAccessTokenResponse accessTokenResponse;
accessTokenResponse = oauthClient.accessToken(this.tokenRequestBuilder.buildBodyMessage());
setAccessToken(accessTokenResponse.getAccessToken());
} catch (OAuthSystemException e) {
throw new IOException(e);
} catch (OAuthProblemException e) {
throw new IOException(e);
}
}
}
public synchronized String getAccessToken() {
return accessToken;
}
public synchronized void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public TokenRequestBuilder getTokenRequestBuilder() {
return tokenRequestBuilder;
}
public void setTokenRequestBuilder(TokenRequestBuilder tokenRequestBuilder) {
this.tokenRequestBuilder = tokenRequestBuilder;
}
public AuthenticationRequestBuilder getAuthenticationRequestBuilder() {
return authenticationRequestBuilder;
}
public void setAuthenticationRequestBuilder(AuthenticationRequestBuilder authenticationRequestBuilder) {
this.authenticationRequestBuilder = authenticationRequestBuilder;
}
}

View File

@@ -0,0 +1,5 @@
package {{invokerPackage}}.auth;
public enum OauthFlow {
accessCode, implicit, password, application
}

View File

@@ -0,0 +1,69 @@
package {{invokerPackage}}.auth;
import java.io.IOException;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.oltu.oauth2.client.HttpClient;
import org.apache.oltu.oauth2.client.request.OAuthClientRequest;
import org.apache.oltu.oauth2.client.response.OAuthClientResponse;
import org.apache.oltu.oauth2.client.response.OAuthClientResponseFactory;
import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
public class OauthOkHttpClient implements HttpClient {
private OkHttpClient client;
public OauthOkHttpClient() {
this.client = new OkHttpClient();
}
public OauthOkHttpClient(OkHttpClient client) {
this.client = client;
}
public <T extends OAuthClientResponse> T execute(OAuthClientRequest request, Map<String, String> headers,
String requestMethod, Class<T> responseClass)
throws OAuthSystemException, OAuthProblemException {
MediaType mediaType = MediaType.parse("application/json");
Request.Builder requestBuilder = new Request.Builder().url(request.getLocationUri());
if(headers != null) {
for (Entry<String, String> entry : headers.entrySet()) {
if (entry.getKey().equalsIgnoreCase("Content-Type")) {
mediaType = MediaType.parse(entry.getValue());
} else {
requestBuilder.addHeader(entry.getKey(), entry.getValue());
}
}
}
RequestBody body = request.getBody() != null ? RequestBody.create(mediaType, request.getBody()) : null;
requestBuilder.method(requestMethod, body);
try {
Response response = client.newCall(requestBuilder.build()).execute();
return OAuthClientResponseFactory.createCustomResponse(
response.body().string(),
response.body().contentType().toString(),
response.code(),
responseClass);
} catch (IOException e) {
throw new OAuthSystemException(e);
}
}
public void shutdown() {
// Nothing to do here
}
}

View File

@@ -112,18 +112,22 @@
<artifactId>swagger-annotations</artifactId>
<version>${swagger-annotations-version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson-version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.squareup.retrofit</groupId>
<artifactId>retrofit</artifactId>
<version>${retrofit-version}</version>
</dependency>
<dependency>
<groupId>org.apache.oltu.oauth2</groupId>
<artifactId>org.apache.oltu.oauth2.client</artifactId>
<version>${oltu-version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp-version}</version>
</dependency>
<!-- test dependencies -->
<dependency>
@@ -137,6 +141,8 @@
<swagger-annotations-version>1.5.0</swagger-annotations-version>
<gson-version>2.3.1</gson-version>
<retrofit-version>1.9.0</retrofit-version>
<okhttp-version>2.4.0</okhttp-version>
<oltu-version>1.0.0</oltu-version>
<maven-plugin-version>1.0.0</maven-plugin-version>
<junit-version>4.12</junit-version>
</properties>

View File

@@ -1,15 +1,17 @@
package {{invokerPackage}};
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.oltu.oauth2.client.request.OAuthClientRequest.AuthenticationRequestBuilder;
import org.apache.oltu.oauth2.client.request.OAuthClientRequest.TokenRequestBuilder;
import retrofit.RestAdapter;
import retrofit.client.OkClient;
import retrofit.converter.ConversionException;
import retrofit.converter.Converter;
import retrofit.converter.GsonConverter;
@@ -17,21 +19,245 @@ import retrofit.mime.TypedByteArray;
import retrofit.mime.TypedInput;
import retrofit.mime.TypedOutput;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.squareup.okhttp.Interceptor;
import com.squareup.okhttp.OkHttpClient;
import {{invokerPackage}}.auth.BasicAuthorization;
import {{invokerPackage}}.auth.ApiKeyAuthorization;
import {{invokerPackage}}.auth.OauthAuthorization;
import {{invokerPackage}}.auth.OauthFlow;
public class ServiceGenerator {
// No need to instantiate this class.
private ServiceGenerator() { }
public static <S> S createService(Class<S> serviceClass) {
Gson gson = new GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
.create();
RestAdapter adapter = new RestAdapter.Builder()
.setEndpoint("{{basePath}}")
.setConverter(new GsonConverterWrapper(gson))
.build();
private Map<String, Interceptor> apiAuthorizations;
private OkHttpClient okClient;
private RestAdapter.Builder adapterBuilder;
return adapter.create(serviceClass);
}
public ServiceGenerator() {
apiAuthorizations = new LinkedHashMap<String, Interceptor>();
createDefaultAdapter();
}
public ServiceGenerator(String[] authNames) {
this();
okClient = new OkHttpClient();
adapterBuilder.setClient(new OkClient(okClient));
for(String authName : authNames) {
if (apiAuthorizations.containsKey(authName)) {
throw new RuntimeException("auth name \"" + authName + "\" already in api authorizations");
}
Interceptor auth;{{#authMethods}}
if (authName == "{{name}}") { {{#isBasic}}
auth = new BasicAuthorization();{{/isBasic}}{{#isApiKey}}
auth = new ApiKeyAuthorization({{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{^isKeyInHeader}}"query"{{/isKeyInHeader}}, "{{keyParamName}}");{{/isApiKey}}{{#isOAuth}}
auth = new OauthAuthorization(OauthFlow.{{flow}}, "{{authorizationUrl}}", "{{tokenUrl}}", "{{#scopes}}{{^-first}}, {{/-first}}{{this}}{{/scopes}}");{{/isOAuth}}
} else {{/authMethods}}{
throw new RuntimeException("auth name \"" + authName + "\" not found in available auth names");
}
apiAuthorizations.put(authName, auth);
}
addAuthsToOkClient(okClient);
}
/**
* Basic constructor for single auth name
* @param authName
*/
public ServiceGenerator(String authName) {
this(new String[]{authName});
}
/**
* Helper constructor for single api key
* @param authName
* @param apiKey
*/
public ServiceGenerator(String authName, String apiKey) {
this(authName);
this.setApiKey(apiKey);
}
/**
* Helper constructor for single basic auth or password oauth2
* @param authName
* @param username
* @param password
*/
public ServiceGenerator(String authName, String username, String password) {
this(authName);
this.setCredentials(username, password);
}
/**
* Helper constructor for single password oauth2
* @param authName
* @param clientId
* @param secret
* @param username
* @param password
*/
public ServiceGenerator(String authName, String clientId, String secret, String username, String password) {
this(authName);
this.getTokenEndPoint()
.setClientId(clientId)
.setClientSecret(secret)
.setUsername(username)
.setPassword(password);
}
public void createDefaultAdapter() {
Gson gson = new GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
.create();
adapterBuilder = new RestAdapter
.Builder()
.setEndpoint("{{basePath}}")
.setConverter(new GsonConverterWrapper(gson));
}
public <S> S createService(Class<S> serviceClass) {
return adapterBuilder.build().create(serviceClass);
}
/**
* Helper method to configure the first api key found
* @param apiKey
*/
private void setApiKey(String apiKey) {
for(Interceptor apiAuthorization : apiAuthorizations.values()) {
if (apiAuthorization instanceof ApiKeyAuthorization) {
ApiKeyAuthorization keyAuth = (ApiKeyAuthorization) apiAuthorization;
keyAuth.setApiKey(apiKey);
return;
}
}
}
/**
* Helper method to configure the username/password for basic auth or password oauth
* @param username
* @param password
*/
private void setCredentials(String username, String password) {
for(Interceptor apiAuthorization : apiAuthorizations.values()) {
if (apiAuthorization instanceof BasicAuthorization) {
BasicAuthorization basicAuth = (BasicAuthorization) apiAuthorization;
basicAuth.setCredentials(username, password);
return;
}
if (apiAuthorization instanceof OauthAuthorization) {
OauthAuthorization oauth = (OauthAuthorization) apiAuthorization;
oauth.getTokenRequestBuilder().setUsername(username).setPassword(password);
return;
}
}
}
/**
* Helper method to configure the token endpoint of the first oauth found in the apiAuthorizations (there should be only one)
* @return
*/
public TokenRequestBuilder getTokenEndPoint() {
for(Interceptor apiAuthorization : apiAuthorizations.values()) {
if (apiAuthorization instanceof OauthAuthorization) {
OauthAuthorization oauth = (OauthAuthorization) apiAuthorization;
return oauth.getTokenRequestBuilder();
}
}
return null;
}
/**
* Helper method to configure authorization endpoint of the first oauth found in the apiAuthorizations (there should be only one)
* @return
*/
public AuthenticationRequestBuilder getAuthorizationEndPoint() {
for(Interceptor apiAuthorization : apiAuthorizations.values()) {
if (apiAuthorization instanceof OauthAuthorization) {
OauthAuthorization oauth = (OauthAuthorization) apiAuthorization;
return oauth.getAuthenticationRequestBuilder();
}
}
return null;
}
/**
* Helper method to pre-set the oauth access token of the first oauth found in the apiAuthorizations (there should be only one)
* @param accessToken
*/
public void setAccessToken(String accessToken) {
for(Interceptor apiAuthorization : apiAuthorizations.values()) {
if (apiAuthorization instanceof OauthAuthorization) {
OauthAuthorization oauth = (OauthAuthorization) apiAuthorization;
oauth.setAccessToken(accessToken);
return;
}
}
}
/**
* Helper method to configure the oauth accessCode/implicit flow parameters
* @param clientId
* @param clientSecret
* @param redirectURI
*/
public void configureAuthorizationFlow(String clientId, String clientSecret, String redirectURI) {
for(Interceptor apiAuthorization : apiAuthorizations.values()) {
if (apiAuthorization instanceof OauthAuthorization) {
OauthAuthorization oauth = (OauthAuthorization) apiAuthorization;
oauth.getTokenRequestBuilder()
.setClientId(clientId)
.setClientSecret(clientSecret)
.setRedirectURI(redirectURI);
oauth.getAuthenticationRequestBuilder()
.setClientId(clientId)
.setRedirectURI(redirectURI);
return;
}
}
}
public Map<String, Interceptor> getApiAuthorizations() {
return apiAuthorizations;
}
public void setApiAuthorizations(Map<String, Interceptor> apiAuthorizations) {
this.apiAuthorizations = apiAuthorizations;
}
public RestAdapter.Builder getAdapterBuilder() {
return adapterBuilder;
}
public void setAdapterBuilder(RestAdapter.Builder adapterBuilder) {
this.adapterBuilder = adapterBuilder;
}
public OkHttpClient getOkClient() {
return okClient;
}
public void addAuthsToOkClient(OkHttpClient okClient) {
for(Interceptor apiAuthorization : apiAuthorizations.values()) {
okClient.interceptors().add(apiAuthorization);
}
}
/**
* Clones the okClient given in parameter, adds the auth interceptors and uses it to configure the RestAdapter
* @param okClient
*/
public void configureFromOkclient(OkHttpClient okClient) {
OkHttpClient clone = okClient.clone();
addAuthsToOkClient(clone);
adapterBuilder.setClient(new OkClient(clone));
}
}
/**