AzureAD for Androidを使用して承認用のライブラリを作成する

したがって、この記事の目的は、例としてAzure AD APIを介した承認を使用してOAuth 2.0を操作する方法を示すことです。 その結果、接続先のプロジェクトから可能な限り最大量のコードを取り出す本格的なモジュールを取得します。



この記事では、Retrofit、rxJava、retrolambdaのライブラリを使用します。 それらの使用は、ボイラープレートを最小限に抑えたいという私の願望のみによるものであり、それ以上のものではありません。 したがって、完全にバニラのアセンブリに翻訳するのに困難はないはずです。



最初に行う必要があるのは、OAuth 2.0承認プロトコルが何であるか(この場合はコードフローのみで使用される)と、目標との関係でどのように見えるかを理解することです。



1.キャッシュされたトークンがある場合、手順4にジャンプします。



2.アプリケーションの認証ページを開く「WebView」 初期化します。



3.ユーザーが入力して[サインイン]をクリックすると、別のページへの自動リダイレクトが行われ、そのクエリパラメーターにはコードパラメーターがあります。 彼が必要です!



4. POSTリクエストを介してトークンのコード交換します。



これは、開発者の観点から直接どういう意味ですか?

最初に行う必要があるのは、必要な定数を個別のクラスに書き込むことです



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"; }
      
      







デフォルトのOkHttpクライアントのパラメーターを割り当てます。



定数クラス
 public class Const { public static int CONNECT_TIMEOUT = 15; public static int WRITE_TIMEOUT = 60; public static int TIMEOUT = 60; }
      
      







それでは、ビジネスに取り掛かりましょう。 実際、ライブラリの最も重要な部分は2つのファイルで構成されます。リクエストシグネチャとAPIファクトリを含むOAuth2



インターフェースと、ニーズに合わせてカスタマイズされたWebViewClientであるOAuth2WebViewClientです。



順番に始めましょう。



トークンのコードを交換するための呼び出し署名は次のとおりです。



  @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 );
      
      





ここで、最初の方法は、段落4で説明したリクエストの署名で、2番目はセッショントークンが1時間有効であることが多いため、定期的に必要となるトークンの更新です。



それでは、APIファクトリーの作成を始めましょう。 それで彼女は何になりますか? Retrofitとの親密な友情の中で、私はこのメカニズムを実装するためのこのオプションに出会いました。



 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(); } }
      
      





このクラスは、前述のインターフェース内になければなりません。



カットの下の完全なコード
 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(); } } }
      
      







トークン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); } }
      
      







カスタムWebViewClientの実装を進めましょう。 これを行うには、正確に何をしたいかを決める必要があります。 実際、初期化されるとき、コールバックまたはBehaviourSubjectsへの参照を入力に与える必要があります(私はこのケースでは前者が好きです)。 それらの3つがあります:最初-コードが正常に受信されるとトリガーされ、2番目-リダイレクト後のURLに「error =」サブストリングがある場合、3番目-他のすべての遷移をリッスンします。



実装するには、 shouldOverrideUrlLoading(WebView webView, String url)



onPageFinished(WebView webView, String url)



2つのWebViewClient



メソッドを再定義する必要があります。



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); } } }
      
      







実際、すべてを使用する準備ができていますが、より柔軟な機能を実現するためにクラスをさらに追加して、定型文をもう少し減らすことができます。



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; } }
      
      







原則として、承認プロセスのみを実装する場合、これを停止できますが、トークンの何らかの操作を実行する必要があることが非常に多いため、トークンマネージャも適切であるように思われました。 したがって、ボーナスとして、以前のクラスに加えて、トークンストレージと単純な更新を実装する別のクラスがあります。 出来上がり:



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() ); } }
      
      







以上で、完全な認証ライブラリが準備できました。 それは簡単にカスタマイズ可能であり、そして最も重要なこと-動作します!



注意点-ダイアログでWebViewを使用する場合は、必ず特定の高さに設定してください。そうでない場合は、単に高さがゼロになります。



この記事は、現在作業中の用語集に基づいて作成されました。これは、Azure ADアカウントを取得するまで待機するという事実に関連しており、Azure ADアカウントでは、さらなる作業に必要な権限をアプリケーションに委任できます。 将来的には、OneNote for Business API(主にAPIのclassNotebooksセクション)を扱うための記事がさらにいくつかあります。



以上です。 建設的な批判に感謝するとともに、あなたの質問にお答えできることを嬉しく思います。



All Articles