This tutorial in the Retrofit series describes and illustrates how to authenticate against an OAuth API from your Android app.
Let's start with an overview of other tutorials within this series.
Retrofit Series Overview
- Getting Started and Creating an Android Client
- Basics of API Description
- Creating a Sustainable Android Client
- URL Handling, Resolution and Parsing
- How to Change API Base Url at Runtime
- Multiple Server Environments (Develop, Staging, Production)
- Share OkHttp Client and Converters between Retrofit Instances
- Upgrade Guide from 1.9
- Beyond Android: Retrofit for Java Projects
- How to use OkHttp 3 with Retrofit 1
- Synchronous and Asynchronous Requests
- Send Objects in Request Body
- Add Custom Request Header
- Manage Request Headers in OkHttp Interceptor
- Dynamic Request Headers with @HeaderMap
- Multiple Query Parameters of Same Name
- Optional Query Parameters
- Send Data Form-Urlencoded
- Send Data Form-Urlencoded Using FieldMap
- How to Add Query Parameters to Every Request
- Add Multiple Query Parameter With QueryMap
- How to Use Dynamic Urls for Requests
- Constant, Default and Logic Values for POST and PUT Requests
- Cancel Requests
- Reuse and Analyze Requests
- Optional Path Parameters
- How to Send Plain Text Request Body
- Customize Network Timeouts
- How to Trust Unsafe SSL certificates (Self-signed, Expired)
- Dynamic Endpoint-Dependent Interceptor Actions
- How to Update Objects on the Server (PUT vs. PATCH)
- How to Delete Objects on the Server
- Introduction to (Multiple) Converters
- Adding & Customizing the Gson Converter
- Implementing Custom Converters
- How to Integrate XML Converter
- Access Mapped Objects and Raw Response Payload
- Supporting JSON and XML Responses Concurrently
- Handling of Empty Server Responses with Custom Converter
- Send JSON Requests and Receive XML Responses (or vice versa)
- Unwrapping Envelope Responses with Custom Converter
- Wrapping Requests in Envelope with Custom Converter
- Define a Custom Response Converter
This post won't go into detail about OAuth itself. It just presents the basic principles and necessary details to understand the authentication flow.
OAuth Basics
OAuth is a token based authorization method which uses an access token for interaction between user and API. OAuth requires several steps and requests against the API to get your access token.
- Register an app for the API you want to develop. Use the developer sites of the public API you're going to develop for.
- Save client id and client secret in your app.
- Request access to user data from your app.
- Use the authorization code to get the access token.
- Use the access token to interact with the API.
Register Your App
Before starting with the implementation you have to register your app for the service/API you want to develop. Once the sign up for your application (which you're going to build) is finished, you'll receive a client id and a client secret. Both values are required to authenticate your app against the service/API.
Create Your Project
We’ll assume you already have an existing project. If you don’t, just go ahead and create an Android project from scratch. When you’re done, move on to the next section and get ready for coding :)
Integrate OAuth
Since we’re using the ServiceGenerator
class from our basic authentication with Retrofit tutorial, we’ll further extend it and add a method to handle the OAuth access token. The snippet below shows the required method within the ServiceGenerator
class. That doesn't mean you should delete the previous created method(s) for basic authentication, since you'll need them for OAuth as well.
Retrofit 2
public class ServiceGenerator {
public static final String API_BASE_URL = "https://your.api-base.url";
private static OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
private static Retrofit.Builder builder =
new Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create());
public static <S> S createService(Class<S> serviceClass) {
return createService(serviceClass, null);
}
public static <S> S createService(
Class<S> serviceClass, String clientId, String clientSecret) {
if (!TextUtils.isEmpty(clientId)
&& !TextUtils.isEmpty(clientSecret)) {
String authToken = Credentials.basic(clientId, clientSecret);
return createService(serviceClass, authToken);
}
return createService(serviceClass, null, null);
}
public static <S> S createService(
Class<S> serviceClass, final String authToken) {
if (!TextUtils.isEmpty(authToken)) {
AuthenticationInterceptor interceptor =
new AuthenticationInterceptor(authToken);
if (!httpClient.interceptors().contains(interceptor)) {
httpClient.addInterceptor(interceptor);
builder.client(httpClient.build());
retrofit = builder.build();
}
}
return retrofit.create(serviceClass);
}
}
Retrofit 1.9
public class ServiceGenerator {
public static final String API_BASE_URL = "https://your.api-base.url";
private static RestAdapter.Builder builder = new RestAdapter.Builder()
.setEndpoint(API_BASE_URL)
.setClient(new OkClient(new OkHttpClient()));
public static <S> S createService(Class<S> serviceClass) {
return createService(serviceClass, null);
}
public static <S> S createService(Class<S> serviceClass, String username, String password) {
// we shortened this part, because it’s covered in
// the previous post on basic authentication with Retrofit
}
public static <S> S createService(Class<S> serviceClass, AccessToken token) {
if (token != null) {
builder.setRequestInterceptor(new RequestInterceptor() {
@Override
public void intercept(RequestFacade request) {
request.addHeader("Accept", "application/json")
request.addHeader("Authorization",
token.getTokenType() + " " + token.getAccessToken());
}
});
}
RestAdapter adapter = builder.build();
return adapter.create(serviceClass);
}
}
We're using the Interceptor
(RequestInterceptor
in Retrofit 1) to set the Authorization field within the HTTP request header. This field consists of two parts: first, the token type which is Bearer for OAuth requests and second, the access token.
As you can see in the code snippet above, the method requires an AccessToken
as third parameter. This class looks like this:
public class AccessToken {
private String accessToken;
private String tokenType;
public String getAccessToken() {
return accessToken;
}
public String getTokenType() {
// OAuth requires uppercase Authorization HTTP header value for token type
if (! Character.isUpperCase(tokenType.charAt(0))) {
tokenType =
Character
.toString(tokenType.charAt(0))
.toUpperCase() + tokenType.substring(1);
}
return tokenType;
}
}
The AccessToken
class consists of two fields: accessToken and tokenType. Since OAuth API implementations require the token type to be in uppercase, we check the styling first. In case it doesn't fit, we update the style. For example, your API returns bearer as token type, any request with this style would result in either 401 Unauthorized, 403 Forbidden or 400 Bad Request.
The HTTP header field will look like the following example when set correctly:
Authorization: Bearer 12345
Integrate OAuth in Your App
First, we'll create a new activity called LoginActivity
. You can use a simple view with only one button (layout code below). Here's the code for the new activity:
public class LoginActivity extends Activity {
// you should either define client id and secret as constants or in string resources
private final String clientId = "your-client-id";
private final String clientSecret = "your-client-secret";
private final String redirectUri = "your://redirecturi";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
Button loginButton (Button) findViewById(R.id.loginbutton);
loginButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(
Intent.ACTION_VIEW,
Uri.parse(ServiceGenerator.API_BASE_URL + "/login" + "?client_id=" + clientId + "&redirect_uri=" + redirectUri));
startActivity(intent);
}
});
}
}
You have to adjust the values for class properties clientId, clientSecret, redirectUri. Also, make sure the partial url for the login is accessible at /login
. If not, update this part to the appropriate one. Further, set an onclick listener for the defined login button within the onCreate
method. Once the onclick event is fired, it creates a new intent showing a webview for the defined Uri. Important: you have to provide your client id and client secret in this request, since the API requires the two parameters for further operation and processing for the app you're using.
Additionally, check the Uri.parse(…)
part. You have to point the url to the login (or authorize) endpoint to show the access rights screen.
The layout for activity_login.xml
can look like this.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Login"
android:id="@+id/loginbutton"
android:gravity="center_vertical|center_horizontal"
/>
</RelativeLayout>
It's just a single button on your view :)
Define Activity and Intent Filter in AndroidManifest.Xml
An intent in Android is a messaging object used to request action or information (communication) from another app or component. The intent filter is used to catch a message from an intent, identified by intent's action, category and data.
The intent filter is required to make Android return to your app, so you can grab further data from the response within your intent. That means, when starting the intent after clicking on your login button within your LoginActivity
, this filter catches any response and makes additional information available. The code below shows the activity definition in AndroidManifest.xml including the intent filter for this activity.
<activity
android:name="com.futurestudio.oauthexample.LoginActivity"
android:label="@string/app_name"
android:configChanges="keyboard|orientation|screenSize">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="redirecturi"
android:scheme="your" />
</intent-filter>
</activity>
Catch the Authorization Code
Ok, until here we have defined the intent to show the webview which presents as a deny or allow view. You'll notice the style of this view when seeing it.
Now we want to get the access token for further API interaction. This token is another two API requests away. First, we need to parse and use the returned authorization code which is part of the response when pressing the allow button within the intent webview. The following method belongs to your LoginActivity
. We separate it since it's easier to explain the contents.
@Override
protected void onResume() {
super.onResume();
// the intent filter defined in AndroidManifest will handle the return from ACTION_VIEW intent
Uri uri = getIntent().getData();
if (uri != null && uri.toString().startsWith(redirectUri)) {
// use the parameter your API exposes for the code (mostly it's "code")
String code = uri.getQueryParameter("code");
if (code != null) {
// get access token
// we'll do that in a minute
} else if (uri.getQueryParameter("error") != null) {
// show an error message here
}
}
}
Your app returns into the onResume
method of Android's lifecycle. Here you can see we're using the getIntent().getData()
methods to retrieve the intent's response.
Now, we don't want to run into any NullPointerException and check the values. Afterwards, we extract the authorization code from query parameters. Imagine the response url when clicking allow like
your://redirecturi?code=1234
and for deny access like
your://redirecturi?error=message
Get Your Access Token
You're almost done, the access token is just one request away. Now that we have the authorization code, we need to request the access token by passing client id, client secret and authorization code to the API.
In the following, we just extend the previous presented onResume
method to do another API request. But first, we have to extend the LoginService
interface and define a method to request the access token. We'll just extend the LoginService
from the basic authentication post with another method called getAccessToken
.
Retrofit 2
public interface LoginService {
@FormUrlEncoded
@POST("/token")
Call<AccessToken> getAccessToken(
@Field("code") String code,
@Field("grant_type") String grantType);
}
Retrofit 1.9
public interface LoginService {
@FormUrlEncoded
@POST("/token")
AccessToken getAccessToken(
@Field("code") String code,
@Field("grant_type") String grantType);
}
This is the interface definition which is passed to ServiceGenerator
to create a Retrofit HTTP client. The getAccessToken
method expects two parameters.
Now the complete code for onResume
to get the token.
Retrofit 2
@Override
protected void onResume() {
super.onResume();
// the intent filter defined in AndroidManifest will handle the return from ACTION_VIEW intent
Uri uri = getIntent().getData();
if (uri != null && uri.toString().startsWith(redirectUri)) {
// use the parameter your API exposes for the code (mostly it's "code")
String code = uri.getQueryParameter("code");
if (code != null) {
// get access token
LoginService loginService =
ServiceGenerator.createService(LoginService.class, clientId, clientSecret);
Call<AccessToken> call = loginService.getAccessToken(code, "authorization_code");
AccessToken accessToken = call.execute().body();
} else if (uri.getQueryParameter("error") != null) {
// show an error message here
}
}
}
Retrofit 1.9
@Override
protected void onResume() {
super.onResume();
// the intent filter defined in AndroidManifest will handle the return from ACTION_VIEW intent
Uri uri = getIntent().getData();
if (uri != null && uri.toString().startsWith(redirectUri)) {
// use the parameter your API exposes for the code (mostly it's "code")
String code = uri.getQueryParameter("code");
if (code != null) {
// get access token
LoginService loginService =
ServiceGenerator.createService(LoginService.class, clientId, clientSecret);
AccessToken accessToken = loginService.getAccessToken(code, "authorization_code");
} else if (uri.getQueryParameter("error") != null) {
// show an error message here
}
}
}
And you're done. You probably have to adjust the grant type value for the API you're requesting. The grant type is passed as the second parameter to the getAccessToken(code, grantType)
method.
Enjoy authenticating to any OAuth API. If you run into questions or problems, just contact us via @futurstud_io.