📜 ⬆️ ⬇️

Create a library for authorization using AzureAD for Android

So, the purpose of this article is to show how to work with OAuth 2.0 using the example of authorization through the Azure AD API. As a result, we will have a full-fledged module that takes out the maximum possible amount of code from the project to which it will be connected.

In this article, the libraries will be used Retrofit, rxJava, retrolambda. Their use is due only to my desire to minimize the boilerplate, and nothing more. Therefore, there should be no difficulties in translating to a fully vanilla assembly.

The first thing we need to do is to realize what the OAuth 2.0 authorization protocol is (in this case only the code flow will be used) and what it will look like in relation to our goal:
')
1. If there is a cached token, skip to step 4.

2. Initialize 'WebView', in which we open the authorization page of our application.

3. After entering the data by the user and clicking on Sign in, there will be an automatic redirect to another page, in the query parameters of which there is a parameter code. He is what we need!

4. Exchange the code for the token via a POST request.

Now what does this mean from the point of view of the developer directly?
The first thing we need to do is to write down the constants we need in separate classes.

Endpoints.class
public class Endpoints { public static final String OAUTH2_BASE_URL = "https://login.microsoftonline.com"; public static final String OAUTH2_ENDPOINT = "/oauth2"; public static final String OAUTH2_AUTHORIZATION_ENDPOINT = "/authorize"; public static final String OAUTH2_TOKEN_ENDPOINT = "/token"; public static final String OAUTH2_TENANT_PATH_FIELD = "/{tenant}"; } 


QueryFields.class
 public class QueryFields { public static final String QUERY_OAUTH2_CLIENT_ID = "client_id"; public static final String QUERY_OAUTH2_RESPONSE_TYPE = "response_type"; public static final String QUERY_OAUTH2_REDIRECT_URI = "redirect_uri"; public static final String QUERY_OAUTH2_RESOURCE = "resource"; } 


RequestFields.class
 public class RequestFields { public static final String OAUTH2_CLIENT_ID = "client_id"; public static final String OAUTH2_GRANT_TYPE = "grant_type"; public static final String OAUTH2_RESOURCE = "resource"; public static final String OAUTH2_CODE = "code"; public static final String OAUTH2_REDIRECT_URI = "redirect_uri"; public static final String OAUTH2_RAW_CODE_QUERY_FIELD = "?code"; public static final String OAUTH2_CODE_QUERY_FIELD = "code"; public static final String OAUTH2_RAW_QEURY_ERROR_FIELD = "error="; } 


RequestFieldValues.class
 public class RequestFieldValues { public static final String TENANT_COMMON = "common"; public static final String GRANT_TYPE_REFRESH_TOKEN = "refresh_token"; } 


ResponseFields.class
 public class ResponseFields { public static final String OAUTH2_TOKEN_TYPE = "token_type"; public static final String OAUTH2_TOKEN_EXPIRES_IN = "expires_in"; public static final String OAUTH2_TOKEN_SCOPE = "scope"; public static final String OAUTH2_TOKEN_EXPIRES_ON = "expires_on"; public static final String OAUTH2_TOKEN_NOT_BEFORE = "not_before"; public static final String OAUTH2_TOKEN_RESOURCE = "resource"; public static final String OAUTH2_TOKEN_ACCESS_TOKEN = "access_token"; public static final String OAUTH2_TOKEN_REFRESH_TOKEN = "refresh_token"; public static final String OAUTH2_TOKEN_ID_TOKEN = "id_token"; } 


Assign the parameters of the default OkHttp client at the same time:

Const.class
 public class Const { public static int CONNECT_TIMEOUT = 15; public static int WRITE_TIMEOUT = 60; public static int TIMEOUT = 60; } 


Now let's get down to business. In fact, the most important part of our library will consist of two files — the OAuth2 interface, which contains the request signatures and the API factory, and OAuth2WebViewClient , which is the customized WebViewClient.

Let's start in order.

The signatures of calls for exchanging code for token are as follows:

  @FormUrlEncoded @POST(OAUTH2_TENANT_PATH_FIELD + OAUTH2_ENDPOINT + OAUTH2_TOKEN_ENDPOINT) Observable<Response<Token>> tradeCodeForToken( @Path(OAUTH2_TENANT_PATH_FIELD) String tenant, @Field(OAUTH2_CLIENT_ID) String clientId, @Field(OAUTH2_GRANT_TYPE) String grantType, @Field(OAUTH2_RESOURCE) String resource, @Field(OAUTH2_CODE) String code, @Field(OAUTH2_REDIRECT_URI) String redirectUri ); 

  @FormUrlEncoded @POST(OAUTH2_TENANT_PATH_FIELD + OAUTH2_ENDPOINT + OAUTH2_TOKEN_ENDPOINT) Observable<Response<Token>> refreshToken( @Path(OAUTH2_TENANT_PATH_FIELD) String tenant, @Field(OAUTH2_CLIENT_ID) String clientId, @Field(OAUTH2_GRANT_TYPE) String grantType, @Field(OAUTH2_RESOURCE) String resource, @Field(OAUTH2_TOKEN_REFRESH_TOKEN) String refreshToken, @Field(OAUTH2_REDIRECT_URI) String redirectUri ); 

Here, the first method is the signature of the request described in clause 4, and the second is the token refresh, which will be periodically required, since the session token is most often validated within an hour.

Now we are going to create an API factory. So what will she represent? During my close friendship with Retrofit, I have come to this version of the implementation of this mechanism:

 class Factory { public static OAuth2 buildOAuth2API(boolean enableDebug) { return buildRetrofit(OAUTH2_BASE_URL, enableDebug).create(OAuth2.class); } protected static Retrofit buildRetrofit(String baseUrl, boolean enableDebug) { return new Retrofit.Builder() .baseUrl(baseUrl) .addConverterFactory(GsonConverterFactory.create(new GsonBuilder().create())) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .client(buildClient(enableDebug)) .build(); } protected static OkHttpClient buildClient(boolean enableDebug) { OkHttpClient.Builder builder = new OkHttpClient.Builder() .connectTimeout(Const.CONNECT_TIMEOUT, TimeUnit.SECONDS) .writeTimeout(Const.WRITE_TIMEOUT, TimeUnit.SECONDS) .readTimeout(Const.TIMEOUT, TimeUnit.SECONDS); if(enableDebug) { builder.addInterceptor( new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY) ); } return builder.build(); } } 

This class must be in the previously described interface.

Full code under the cut
 public interface OAuth2 { /** The request signature that returns a deserialized token */ @FormUrlEncoded @POST(OAUTH2_TENANT_PATH_FIELD + OAUTH2_ENDPOINT + OAUTH2_TOKEN_ENDPOINT) Observable<Response<Token>> tradeCodeForToken( @Path(OAUTH2_TENANT_PATH_FIELD) String tenant, @Field(OAUTH2_CLIENT_ID) String clientId, @Field(OAUTH2_GRANT_TYPE) String grantType, @Field(OAUTH2_RESOURCE) String resource, @Field(OAUTH2_CODE) String code, @Field(OAUTH2_REDIRECT_URI) String redirectUri ); /** The request signature that returns a raw json object instead of deserealized token */ @FormUrlEncoded @POST(OAUTH2_TENANT_PATH_FIELD + OAUTH2_ENDPOINT + OAUTH2_TOKEN_ENDPOINT) Observable<Response<JsonObject>> tradeCodeForTokenRaw( @Path(OAUTH2_TENANT_PATH_FIELD) String tenant, @Field(OAUTH2_CLIENT_ID) String clientId, @Field(OAUTH2_GRANT_TYPE) String grantType, @Field(OAUTH2_RESOURCE) String resource, @Field(OAUTH2_CODE) String code, @Field(OAUTH2_REDIRECT_URI) String redirectUri ); /** The request signature that allows refreshing token */ @FormUrlEncoded @POST(OAUTH2_TENANT_PATH_FIELD + OAUTH2_ENDPOINT + OAUTH2_TOKEN_ENDPOINT) Observable<Response<Token>> refreshToken( @Path(OAUTH2_TENANT_PATH_FIELD) String tenant, @Field(OAUTH2_CLIENT_ID) String clientId, @Field(OAUTH2_GRANT_TYPE) String grantType, @Field(OAUTH2_RESOURCE) String resource, @Field(OAUTH2_TOKEN_REFRESH_TOKEN) String refreshToken, @Field(OAUTH2_REDIRECT_URI) String redirectUri ); /** The request signature that allows refreshing token and returns a raw json instead of deserialized token */ @FormUrlEncoded @POST(OAUTH2_TENANT_PATH_FIELD + OAUTH2_ENDPOINT + OAUTH2_TOKEN_ENDPOINT) Observable<Response<Token>> refreshTokenRaw( @Path(OAUTH2_TENANT_PATH_FIELD) String tenant, @Field(OAUTH2_CLIENT_ID) String clientId, @Field(OAUTH2_GRANT_TYPE) String grantType, @Field(OAUTH2_RESOURCE) String resource, @Field(OAUTH2_TOKEN_REFRESH_TOKEN) String refreshToken, @Field(OAUTH2_REDIRECT_URI) String redirectUri ); class Factory { public static OAuth2 buildOAuth2API(boolean enableDebug) { return buildRetrofit(OAUTH2_BASE_URL, enableDebug).create(OAuth2.class); } protected static Retrofit buildRetrofit(String baseUrl, boolean enableDebug) { return new Retrofit.Builder() .baseUrl(baseUrl) .addConverterFactory(GsonConverterFactory.create(new GsonBuilder().create())) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .client(buildClient(enableDebug)) .build(); } protected static OkHttpClient buildClient(boolean enableDebug) { OkHttpClient.Builder builder = new OkHttpClient.Builder() .connectTimeout(Const.CONNECT_TIMEOUT, TimeUnit.SECONDS) .writeTimeout(Const.WRITE_TIMEOUT, TimeUnit.SECONDS) .readTimeout(Const.TIMEOUT, TimeUnit.SECONDS); if(enableDebug) { builder.addInterceptor( new HttpLoggingInterceptor().setLevel( HttpLoggingInterceptor.Level.BODY ) ); } return builder.build(); } } } 


Token dto
 public class Token { @SerializedName(OAUTH2_TOKEN_TYPE) private String tokenType; @SerializedName(OAUTH2_TOKEN_EXPIRES_IN) private String expiresIn; @SerializedName(OAUTH2_TOKEN_SCOPE) private String scope; @SerializedName(OAUTH2_TOKEN_EXPIRES_ON) private String expiresOn; @SerializedName(OAUTH2_TOKEN_NOT_BEFORE) private String notBefore; @SerializedName(OAUTH2_TOKEN_RESOURCE) private String resource; @SerializedName(OAUTH2_TOKEN_ACCESS_TOKEN) private String accessToken; @SerializedName(OAUTH2_TOKEN_REFRESH_TOKEN) private String refreshToken; @SerializedName(OAUTH2_TOKEN_ID_TOKEN) private String idToken; public Token(String tokenType, String expiresIn, String scope, String expiresOn, String notBefore, String resource, String accessToken, String refreshToken, String idToken) { this.tokenType = tokenType; this.expiresIn = expiresIn; this.scope = scope; this.expiresOn = expiresOn; this.notBefore = notBefore; this.resource = resource; this.accessToken = accessToken; this.refreshToken = refreshToken; this.idToken = idToken; } public String getTokenType() { return tokenType; } public void setTokenType(String tokenType) { this.tokenType = tokenType; } public String getExpiresIn() { return expiresIn; } public void setExpiresIn(String expiresIn) { this.expiresIn = expiresIn; } public String getScope() { return scope; } public void setScope(String scope) { this.scope = scope; } public String getExpiresOn() { return expiresOn; } public void setExpiresOn(String expiresOn) { this.expiresOn = expiresOn; } public String getNotBefore() { return notBefore; } public void setNotBefore(String notBefore) { this.notBefore = notBefore; } public String getResource() { return resource; } public void setResource(String resource) { this.resource = resource; } public String getAccessToken() { return accessToken; } public void setAccessToken(String accessToken) { this.accessToken = accessToken; } public String getRefreshToken() { return refreshToken; } public void setRefreshToken(String refreshToken) { this.refreshToken = refreshToken; } public String getIdToken() { return idToken; } public void setIdToken(String idToken) { this.idToken = idToken; } @Override public String toString() { return "MicrosoftAzureOAuthToken{" + "tokenType='" + tokenType + '\'' + ", expiresIn='" + expiresIn + '\'' + ", scope='" + scope + '\'' + ", expiresOn='" + expiresOn + '\'' + ", notBefore='" + notBefore + '\'' + ", resource='" + resource + '\'' + ", accessToken='" + accessToken + '\'' + ", refreshToken='" + refreshToken + '\'' + ", idToken='" + idToken + '\'' + '}'; } public String toJsonString() { return new Gson().toJson(this, Token.class); } public static Token fromJsonString(String jsonString) { return new Gson().fromJson(jsonString, Token.class); } } 


Let's start implementation of custom WebViewClient. To do this, we need to decide what we want to do. In fact, when it is initialized, the input should contain links to callbacks, or to BehaviourSubjects (to taste, I like the first one in this case). There will be three of them in total: the first will trigger when the code is received successfully, the second - if there is an 'error =' substring in the url after the redirect and the third one that listens to all other transitions.

To implement, we need to override two WebViewClient methods: shouldOverrideUrlLoading(WebView webView, String url) and onPageFinished(WebView webView, String url) .

OAuth2WebViewClient
 public class OAuth2WebViewClient extends WebViewClient { private Action1<String> onSuccess; private Action1<String> onError; private Action1<String> onUnknownUrlPassed; public OAuth2WebViewClient(Action1<String> onSuccess, Action1<String> onError, Action1<String> onUnknownUrlPassed) { this.onSuccess = onSuccess; this.onUnknownUrlPassed = onUnknownUrlPassed; this.onError = onError; } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if(url.contains(OAUTH2_RAW_CODE_QUERY_FIELD) || url.contains(OAUTH2_RAW_QEURY_ERROR_FIELD)) { return true; } else { view.loadUrl(url); return false; } } @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); if(url.contains(OAUTH2_RAW_CODE_QUERY_FIELD)) { Uri uri = Uri.parse(url); onSuccess.call(uri.getQueryParameter(OAUTH2_CODE_QUERY_FIELD)); } else if(url.contains(OAUTH2_RAW_CODE_QUERY_FIELD)) { onError.call(url); } else { onUnknownUrlPassed.call(url); } } } 


In fact, everything is ready for use, but you can add a couple more classes for more flexible functionality in order to reduce the boilerplate by a bit more.

AzureAuthenticationWebView
 public class AzureAuthenticationWebView extends WebView { public AzureAuthenticationWebView(Context context) { super(context); } public AzureAuthenticationWebView(Context context, AttributeSet attrs) { super(context, attrs); } public AzureAuthenticationWebView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public AzureAuthenticationWebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } public void init(OAuth2WebViewClient client, String query) { WebSettings settings = this.getSettings(); settings.setJavaScriptEnabled(true); settings.setSupportMultipleWindows(true); this.setWebViewClient(client); this.loadUrl(query); } } 


AzureStorageManager
 public class AzureStorageManager { private ObscuredSharedPreferences preferences; public AzureStorageManager(ObscuredSharedPreferences preferences) { this.preferences = preferences; } public Token readToken() { String rawToken = preferences.getString(TOKEN_JSON_KEY, ""); return Token.fromJsonString(rawToken); } public void writeToken(Token token) { ObscuredSharedPreferences.Editor editor = preferences.edit(); editor.putString(TOKEN_JSON_KEY, token.toJsonString()); editor.commit(); } } 


QueryStringBuilder
 public class QueryStringBuilder { private String query; public QueryStringBuilder(String tenant) { query = OAUTH2_BASE_URL.concat("/").concat(tenant).concat(OAUTH2_ENDPOINT).concat(OAUTH2_AUTHORIZATION_ENDPOINT).concat("?"); } public QueryStringBuilder setClientId(String clientId) { query = prepareQuery(query); query = query.concat(QUERY_OAUTH2_CLIENT_ID).concat("=").concat(clientId); return this; } public QueryStringBuilder setResponseType(String responseType) { query = prepareQuery(query); query = query.concat(QUERY_OAUTH2_RESPONSE_TYPE).concat("=").concat(responseType); return this; } public QueryStringBuilder setRedirectUri(String redirectUri) { query = prepareQuery(query); query = query.concat(QUERY_OAUTH2_REDIRECT_URI).concat("=").concat(redirectUri); return this; } public QueryStringBuilder setResource(String resource) { query = prepareQuery(query); query = query.concat(QUERY_OAUTH2_RESOURCE).concat("=").concat(resource); return this; } public String build() { return query; } private String prepareQuery(String query) { if(query != null && query.length() != 0 && !(String.valueOf(query.charAt(query.length() - 1)).equals("?"))) { query = query.concat("&"); } return query; } } 


In principle, this can be stopped if we only implement the authorization process, but it seemed to me that the token manager would also be relevant, since very often I had to perform some manipulations with tokens. Therefore, as a bonus, there is another class that, in addition to the previous ones, implements the storage of tokens, as well as a simple refresh. Voila:

Tokenmanager
 public class TokenManager { private Subscription subscription = Subscriptions.empty(); private AzureStorageManager storageManager; private String tenantType; private String clientId; private String redirectUri; public TokenManager(AzureStorageManager storageManager, String tenantType, String clientId, String redirectUri) { this.storageManager = storageManager; this.tenantType = tenantType; this.clientId = clientId; this.redirectUri = redirectUri; } /** Performs (code -> token) exchange using MS OAuth2 API * Caches the token if the response code is equals to HTTP_OK */ public void tradeCodeForToken(String code, String resource, final Action1<Token> onSuccess, Action1<Integer> onHttpError, Action1<Throwable> onFailure) { subscription = OAuth2.Factory.buildOAuth2API(false) .tradeCodeForToken( tenantType, clientId, GRANT_TYPE_REFRESH_TOKEN, resource, code, redirectUri ) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .filter(response -> { if(response.code() != HTTP_OK) { onHttpError.call(response.code()); return false; } return true; }) .map(Response::body) .subscribe( token -> { storageManager.writeToken(token); onSuccess.call(token); }, e -> { onFailure.call(e); subscription.unsubscribe(); }, () -> subscription.unsubscribe() ); } /** Refreshes expired token * Caches the token if the response code is equals to HTTP_OK */ public void refreshToken(Token expiredToken, final Action1<Token> onSuccess, Action1<Integer> onHttpError, Action1<Throwable> onFailure) { subscription = OAuth2.Factory.buildOAuth2API(false) .refreshToken( tenantType, clientId, GRANT_TYPE_REFRESH_TOKEN, expiredToken.getResource(), expiredToken.getRefreshToken(), redirectUri ) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .filter(response -> { if(response.code() != HTTP_OK) { onHttpError.call(response.code()); return false; } return true; }) .map(Response::body) .subscribe( token -> { storageManager.writeToken(token); onSuccess.call(token); }, e -> { onFailure.call(e); subscription.unsubscribe(); }, () -> subscription.unsubscribe() ); } } 


That's it, the full authorization library is ready. It is easily customizable, and, most importantly, it works!

A small note - in case you want to use WebView in the dialogue - be sure to set a specific height for it, because otherwise it will simply have a zero height.

The article was written based on my coursework, which I am doing at the moment, due to the fact that I am waiting for me to be given an Azure AD account in which you can delegate the permissions necessary for the further work to applications. In the future there will be a few more articles devoted to working with the OneNote for Business API (mainly with the classNotebooks section of their api).

That's all. I would be grateful for constructive criticism, and I will also be happy to answer your questions.

Source: https://habr.com/ru/post/313454/


All Articles