001/* 002 * Copyright 2015 The AppAuth for Android Authors. All Rights Reserved. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 005 * in compliance with the License. You may obtain a copy of the License at 006 * 007 * http://www.apache.org/licenses/LICENSE-2.0 008 * 009 * Unless required by applicable law or agreed to in writing, software distributed under the 010 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 011 * express or implied. See the License for the specific language governing permissions and 012 * limitations under the License. 013 */ 014 015package net.openid.appauth; 016 017import static net.openid.appauth.Preconditions.checkNotNull; 018 019import android.annotation.TargetApi; 020import android.app.Activity; 021import android.app.PendingIntent; 022import android.content.ActivityNotFoundException; 023import android.content.Context; 024import android.content.ContextWrapper; 025import android.content.Intent; 026import android.net.Uri; 027import android.os.AsyncTask; 028import android.os.Build; 029import android.text.TextUtils; 030import androidx.annotation.NonNull; 031import androidx.annotation.Nullable; 032import androidx.annotation.VisibleForTesting; 033import androidx.browser.customtabs.CustomTabsIntent; 034 035import net.openid.appauth.AuthorizationException.GeneralErrors; 036import net.openid.appauth.AuthorizationException.RegistrationRequestErrors; 037import net.openid.appauth.AuthorizationException.TokenRequestErrors; 038import net.openid.appauth.IdToken.IdTokenException; 039import net.openid.appauth.browser.BrowserDescriptor; 040import net.openid.appauth.browser.BrowserSelector; 041import net.openid.appauth.browser.CustomTabManager; 042import net.openid.appauth.connectivity.ConnectionBuilder; 043import net.openid.appauth.internal.Logger; 044import net.openid.appauth.internal.UriUtil; 045import org.json.JSONException; 046import org.json.JSONObject; 047 048import java.io.IOException; 049import java.io.InputStream; 050import java.io.OutputStreamWriter; 051import java.net.HttpURLConnection; 052import java.net.URLConnection; 053import java.util.Map; 054 055 056/** 057 * Dispatches requests to an OAuth2 authorization service. Note that instances of this class 058 * _must be manually disposed_ when no longer required, to avoid leaks 059 * (see {@link #dispose()}. 060 */ 061public class AuthorizationService { 062 063 @VisibleForTesting 064 Context mContext; 065 066 @NonNull 067 private final AppAuthConfiguration mClientConfiguration; 068 069 @NonNull 070 private final CustomTabManager mCustomTabManager; 071 072 @Nullable 073 private final BrowserDescriptor mBrowser; 074 075 private boolean mDisposed = false; 076 077 /** 078 * Creates an AuthorizationService instance, using the 079 * {@link AppAuthConfiguration#DEFAULT default configuration}. Note that 080 * instances of this class must be manually disposed when no longer required, to avoid 081 * leaks (see {@link #dispose()}. 082 */ 083 public AuthorizationService(@NonNull Context context) { 084 this(context, AppAuthConfiguration.DEFAULT); 085 } 086 087 /** 088 * Creates an AuthorizationService instance, using the specified configuration. Note that 089 * instances of this class must be manually disposed when no longer required, to avoid 090 * leaks (see {@link #dispose()}. 091 */ 092 public AuthorizationService( 093 @NonNull Context context, 094 @NonNull AppAuthConfiguration clientConfiguration) { 095 this(context, 096 clientConfiguration, 097 BrowserSelector.select( 098 context, 099 clientConfiguration.getBrowserMatcher()), 100 new CustomTabManager(context)); 101 } 102 103 /** 104 * Constructor that injects a url builder into the service for testing. 105 */ 106 @VisibleForTesting 107 AuthorizationService(@NonNull Context context, 108 @NonNull AppAuthConfiguration clientConfiguration, 109 @Nullable BrowserDescriptor browser, 110 @NonNull CustomTabManager customTabManager) { 111 mContext = checkNotNull(context); 112 mClientConfiguration = clientConfiguration; 113 mCustomTabManager = customTabManager; 114 mBrowser = browser; 115 116 if (browser != null && browser.useCustomTab) { 117 mCustomTabManager.bind(browser.packageName); 118 } 119 } 120 121 public CustomTabManager getCustomTabManager() { 122 return mCustomTabManager; 123 } 124 125 /** 126 * Returns the BrowserDescriptor of the chosen browser. 127 * Can for example be used to set the browsers package name to a CustomTabsIntent. 128 */ 129 public BrowserDescriptor getBrowserDescriptor() { 130 return mBrowser; 131 } 132 133 /** 134 * Creates a custom tab builder, that will use a tab session from an existing connection to 135 * a web browser, if available. 136 */ 137 public CustomTabsIntent.Builder createCustomTabsIntentBuilder(Uri... possibleUris) { 138 checkNotDisposed(); 139 return mCustomTabManager.createTabBuilder(possibleUris); 140 } 141 142 /** 143 * Sends an authorization request to the authorization service, using a 144 * [custom tab](https://developer.chrome.com/multidevice/android/customtabs) 145 * if available, or a browser instance. 146 * The parameters of this request are determined by both the authorization service 147 * configuration and the provided {@link AuthorizationRequest request object}. Upon completion 148 * of this request, the provided {@link PendingIntent completion PendingIntent} will be invoked. 149 * If the user cancels the authorization request, the current activity will regain control. 150 */ 151 public void performAuthorizationRequest( 152 @NonNull AuthorizationRequest request, 153 @NonNull PendingIntent completedIntent) { 154 performAuthorizationRequest( 155 request, 156 completedIntent, 157 null, 158 createCustomTabsIntentBuilder().build()); 159 } 160 161 /** 162 * Sends an authorization request to the authorization service, using a 163 * [custom tab](https://developer.chrome.com/multidevice/android/customtabs) 164 * if available, or a browser instance. 165 * The parameters of this request are determined by both the authorization service 166 * configuration and the provided {@link AuthorizationRequest request object}. Upon completion 167 * of this request, the provided {@link PendingIntent completion PendingIntent} will be invoked. 168 * If the user cancels the authorization request, the provided 169 * {@link PendingIntent cancel PendingIntent} will be invoked. 170 */ 171 public void performAuthorizationRequest( 172 @NonNull AuthorizationRequest request, 173 @NonNull PendingIntent completedIntent, 174 @NonNull PendingIntent canceledIntent) { 175 performAuthorizationRequest( 176 request, 177 completedIntent, 178 canceledIntent, 179 createCustomTabsIntentBuilder().build()); 180 } 181 182 /** 183 * Sends an authorization request to the authorization service, using a 184 * [custom tab](https://developer.chrome.com/multidevice/android/customtabs). 185 * The parameters of this request are determined by both the authorization service 186 * configuration and the provided {@link AuthorizationRequest request object}. Upon completion 187 * of this request, the provided {@link PendingIntent completion PendingIntent} will be invoked. 188 * If the user cancels the authorization request, the current activity will regain control. 189 * 190 * @param customTabsIntent 191 * The intent that will be used to start the custom tab. It is recommended that this intent 192 * be created with the help of {@link #createCustomTabsIntentBuilder(Uri[])}, which will 193 * ensure that a warmed-up version of the browser will be used, minimizing latency. 194 */ 195 public void performAuthorizationRequest( 196 @NonNull AuthorizationRequest request, 197 @NonNull PendingIntent completedIntent, 198 @NonNull CustomTabsIntent customTabsIntent) { 199 performAuthorizationRequest( 200 request, 201 completedIntent, 202 null, 203 customTabsIntent); 204 } 205 206 /** 207 * Sends an authorization request to the authorization service, using a 208 * [custom tab](https://developer.chrome.com/multidevice/android/customtabs). 209 * The parameters of this request are determined by both the authorization service 210 * configuration and the provided {@link AuthorizationRequest request object}. Upon completion 211 * of this request, the provided {@link PendingIntent completion PendingIntent} will be invoked. 212 * If the user cancels the authorization request, the provided 213 * {@link PendingIntent cancel PendingIntent} will be invoked. 214 * 215 * @param customTabsIntent 216 * The intent that will be used to start the custom tab. It is recommended that this intent 217 * be created with the help of {@link #createCustomTabsIntentBuilder(Uri[])}, which will 218 * ensure that a warmed-up version of the browser will be used, minimizing latency. 219 * 220 * @throws android.content.ActivityNotFoundException if no suitable browser is available to 221 * perform the authorization flow. 222 */ 223 public void performAuthorizationRequest( 224 @NonNull AuthorizationRequest request, 225 @NonNull PendingIntent completedIntent, 226 @Nullable PendingIntent canceledIntent, 227 @NonNull CustomTabsIntent customTabsIntent) { 228 performAuthManagementRequest( 229 request, 230 completedIntent, 231 canceledIntent, 232 customTabsIntent); 233 } 234 235 /** 236 * Sends an end session request to the authorization service, using a 237 * [custom tab](https://developer.chrome.com/multidevice/android/customtabs) 238 * if available, or a browser instance. 239 * The parameters of this request are determined by both the authorization service 240 * configuration and the provided {@link EndSessionRequest request object}. Upon completion 241 * of this request, the provided {@link PendingIntent completion PendingIntent} will be invoked. 242 * If the user cancels the authorization request, the current activity will regain control. 243 */ 244 public void performEndSessionRequest( 245 @NonNull EndSessionRequest request, 246 @NonNull PendingIntent completedIntent) { 247 performEndSessionRequest( 248 request, 249 completedIntent, 250 null, 251 createCustomTabsIntentBuilder().build()); 252 } 253 254 /** 255 * Sends an end session request to the authorization service, using a 256 * [custom tab](https://developer.chrome.com/multidevice/android/customtabs) 257 * if available, or a browser instance. 258 * The parameters of this request are determined by both the authorization service 259 * configuration and the provided {@link EndSessionRequest request object}. Upon completion 260 * of this request, the provided {@link PendingIntent completion PendingIntent} will be invoked. 261 * If the user cancels the authorization request, the provided 262 * {@link PendingIntent cancel PendingIntent} will be invoked. 263 */ 264 public void performEndSessionRequest( 265 @NonNull EndSessionRequest request, 266 @NonNull PendingIntent completedIntent, 267 @NonNull PendingIntent canceledIntent) { 268 performEndSessionRequest( 269 request, 270 completedIntent, 271 canceledIntent, 272 createCustomTabsIntentBuilder().build()); 273 } 274 275 /** 276 * Sends an end session request to the authorization service, using a 277 * [custom tab](https://developer.chrome.com/multidevice/android/customtabs). 278 * The parameters of this request are determined by both the authorization service 279 * configuration and the provided {@link EndSessionRequest request object}. Upon completion 280 * of this request, the provided {@link PendingIntent completion PendingIntent} will be invoked. 281 * If the user cancels the authorization request, the current activity will regain control. 282 * 283 * @param customTabsIntent 284 * The intent that will be used to start the custom tab. It is recommended that this intent 285 * be created with the help of {@link #createCustomTabsIntentBuilder(Uri[])}, which will 286 * ensure that a warmed-up version of the browser will be used, minimizing latency. 287 */ 288 public void performEndSessionRequest( 289 @NonNull EndSessionRequest request, 290 @NonNull PendingIntent completedIntent, 291 @NonNull CustomTabsIntent customTabsIntent) { 292 performEndSessionRequest( 293 request, 294 completedIntent, 295 null, 296 customTabsIntent); 297 } 298 299 /** 300 * Sends an end session request to the authorization service, using a 301 * [custom tab](https://developer.chrome.com/multidevice/android/customtabs). 302 * The parameters of this request are determined by both the authorization service 303 * configuration and the provided {@link EndSessionRequest request object}. Upon completion 304 * of this request, the provided {@link PendingIntent completion PendingIntent} will be invoked. 305 * If the user cancels the authorization request, the provided 306 * {@link PendingIntent cancel PendingIntent} will be invoked. 307 * 308 * @param customTabsIntent 309 * The intent that will be used to start the custom tab. It is recommended that this intent 310 * be created with the help of {@link #createCustomTabsIntentBuilder(Uri[])}, which will 311 * ensure that a warmed-up version of the browser will be used, minimizing latency. 312 * 313 * @throws android.content.ActivityNotFoundException if no suitable browser is available to 314 * perform the authorization flow. 315 */ 316 public void performEndSessionRequest( 317 @NonNull EndSessionRequest request, 318 @NonNull PendingIntent completedIntent, 319 @Nullable PendingIntent canceledIntent, 320 @NonNull CustomTabsIntent customTabsIntent) { 321 performAuthManagementRequest( 322 request, 323 completedIntent, 324 canceledIntent, 325 customTabsIntent); 326 } 327 328 private void performAuthManagementRequest( 329 @NonNull AuthorizationManagementRequest request, 330 @NonNull PendingIntent completedIntent, 331 @Nullable PendingIntent canceledIntent, 332 @NonNull CustomTabsIntent customTabsIntent) { 333 334 checkNotDisposed(); 335 checkNotNull(request); 336 checkNotNull(completedIntent); 337 checkNotNull(customTabsIntent); 338 339 Intent authIntent = prepareAuthorizationRequestIntent(request, customTabsIntent); 340 Intent startIntent = AuthorizationManagementActivity.createStartIntent( 341 mContext, 342 request, 343 authIntent, 344 completedIntent, 345 canceledIntent); 346 347 // Calling start activity from outside an activity requires FLAG_ACTIVITY_NEW_TASK. 348 if (!isActivity(mContext)) { 349 startIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 350 } 351 mContext.startActivity(startIntent); 352 } 353 354 private boolean isActivity(Context context) { 355 while (context instanceof ContextWrapper) { 356 if (context instanceof Activity) { 357 return true; 358 } 359 context = ((ContextWrapper) context).getBaseContext(); 360 } 361 return false; 362 } 363 364 /** 365 * Constructs an intent that encapsulates the provided request and custom tabs intent, 366 * and is intended to be launched via {@link Activity#startActivityForResult}. 367 * The parameters of this request are determined by both the authorization service 368 * configuration and the provided {@link AuthorizationRequest request object}. Upon completion 369 * of this request, the activity that gets launched will call {@link Activity#setResult} with 370 * {@link Activity#RESULT_OK} and an {@link Intent} containing authorization completion 371 * information. If the user presses the back button or closes the browser tab, the launched 372 * activity will call {@link Activity#setResult} with 373 * {@link Activity#RESULT_CANCELED} without a data {@link Intent}. Note that 374 * {@link Activity#RESULT_OK} indicates the authorization request completed, 375 * not necessarily that it was a successful authorization. 376 * 377 * @param customTabsIntent 378 * The intent that will be used to start the custom tab. It is recommended that this intent 379 * be created with the help of {@link #createCustomTabsIntentBuilder(Uri[])}, which will 380 * ensure that a warmed-up version of the browser will be used, minimizing latency. 381 * 382 * @throws android.content.ActivityNotFoundException if no suitable browser is available to 383 * perform the authorization flow. 384 */ 385 @TargetApi(Build.VERSION_CODES.LOLLIPOP) 386 public Intent getAuthorizationRequestIntent( 387 @NonNull AuthorizationRequest request, 388 @NonNull CustomTabsIntent customTabsIntent) { 389 390 Intent authIntent = prepareAuthorizationRequestIntent(request, customTabsIntent); 391 return AuthorizationManagementActivity.createStartForResultIntent( 392 mContext, 393 request, 394 authIntent); 395 } 396 397 /** 398 * Constructs an intent that encapsulates the provided request and a default custom tabs intent, 399 * and is intended to be launched via {@link Activity#startActivityForResult} 400 * When started, the intent launches an {@link Activity} that sends an authorization request 401 * to the authorization service, using a 402 * [custom tab](https://developer.chrome.com/multidevice/android/customtabs). 403 * The parameters of this request are determined by both the authorization service 404 * configuration and the provided {@link AuthorizationRequest request object}. Upon completion 405 * of this request, the activity that gets launched will call {@link Activity#setResult} with 406 * {@link Activity#RESULT_OK} and an {@link Intent} containing authorization completion 407 * information. If the user presses the back button or closes the browser tab, the launched 408 * activity will call {@link Activity#setResult} with 409 * {@link Activity#RESULT_CANCELED} without a data {@link Intent}. Note that 410 * {@link Activity#RESULT_OK} indicates the authorization request completed, 411 * not necessarily that it was a successful authorization. 412 * 413 * @throws android.content.ActivityNotFoundException if no suitable browser is available to 414 * perform the authorization flow. 415 */ 416 @TargetApi(Build.VERSION_CODES.LOLLIPOP) 417 public Intent getAuthorizationRequestIntent( 418 @NonNull AuthorizationRequest request) { 419 return getAuthorizationRequestIntent(request, createCustomTabsIntentBuilder().build()); 420 } 421 422 /** 423 * Constructs an intent that encapsulates the provided request and custom tabs intent, 424 * and is intended to be launched via {@link Activity#startActivityForResult}. 425 * The parameters of this request are determined by both the authorization service 426 * configuration and the provided {@link AuthorizationRequest request object}. Upon completion 427 * of this request, the activity that gets launched will call {@link Activity#setResult} with 428 * {@link Activity#RESULT_OK} and an {@link Intent} containing authorization completion 429 * information. If the user presses the back button or closes the browser tab, the launched 430 * activity will call {@link Activity#setResult} with 431 * {@link Activity#RESULT_CANCELED} without a data {@link Intent}. Note that 432 * {@link Activity#RESULT_OK} indicates the authorization request completed, 433 * not necessarily that it was a successful authorization. 434 * 435 * @param customTabsIntent 436 * The intent that will be used to start the custom tab. It is recommended that this intent 437 * be created with the help of {@link #createCustomTabsIntentBuilder(Uri[])}, which will 438 * ensure that a warmed-up version of the browser will be used, minimizing latency. 439 * 440 * @throws android.content.ActivityNotFoundException if no suitable browser is available to 441 * perform the authorization flow. 442 */ 443 @TargetApi(Build.VERSION_CODES.LOLLIPOP) 444 public Intent getEndSessionRequestIntent( 445 @NonNull EndSessionRequest request, 446 @NonNull CustomTabsIntent customTabsIntent) { 447 448 Intent authIntent = prepareAuthorizationRequestIntent(request, customTabsIntent); 449 return AuthorizationManagementActivity.createStartForResultIntent( 450 mContext, 451 request, 452 authIntent); 453 } 454 455 /** 456 * Constructs an intent that encapsulates the provided request and a default custom tabs intent, 457 * and is intended to be launched via {@link Activity#startActivityForResult} 458 * When started, the intent launches an {@link Activity} that sends an authorization request 459 * to the authorization service, using a 460 * [custom tab](https://developer.chrome.com/multidevice/android/customtabs). 461 * The parameters of this request are determined by both the authorization service 462 * configuration and the provided {@link EndSessionRequest request object}. Upon completion 463 * of this request, the activity that gets launched will call {@link Activity#setResult} with 464 * {@link Activity#RESULT_OK} and an {@link Intent} containing authorization completion 465 * information. If the user presses the back button or closes the browser tab, the launched 466 * activity will call {@link Activity#setResult} with 467 * {@link Activity#RESULT_CANCELED} without a data {@link Intent}. Note that 468 * {@link Activity#RESULT_OK} indicates the authorization request completed, 469 * not necessarily that it was a successful authorization. 470 * 471 * @throws android.content.ActivityNotFoundException if no suitable browser is available to 472 * perform the authorization flow. 473 */ 474 @TargetApi(Build.VERSION_CODES.LOLLIPOP) 475 public Intent getEndSessionRequestIntent( 476 @NonNull EndSessionRequest request) { 477 return getEndSessionRequestIntent(request, createCustomTabsIntentBuilder().build()); 478 } 479 480 /** 481 * Sends a request to the authorization service to exchange a code granted as part of an 482 * authorization request for a token. The result of this request will be sent to the provided 483 * callback handler. 484 */ 485 public void performTokenRequest( 486 @NonNull TokenRequest request, 487 @NonNull TokenResponseCallback callback) { 488 performTokenRequest(request, NoClientAuthentication.INSTANCE, callback); 489 } 490 491 /** 492 * Sends a request to the authorization service to exchange a code granted as part of an 493 * authorization request for a token. The result of this request will be sent to the provided 494 * callback handler. 495 */ 496 public void performTokenRequest( 497 @NonNull TokenRequest request, 498 @NonNull ClientAuthentication clientAuthentication, 499 @NonNull TokenResponseCallback callback) { 500 checkNotDisposed(); 501 Logger.debug("Initiating code exchange request to %s", 502 request.configuration.tokenEndpoint); 503 new TokenRequestTask( 504 request, 505 clientAuthentication, 506 mClientConfiguration.getConnectionBuilder(), 507 SystemClock.INSTANCE, 508 callback, 509 mClientConfiguration.getSkipIssuerHttpsCheck()) 510 .execute(); 511 } 512 513 /** 514 * Sends a request to the authorization service to dynamically register a client. 515 * The result of this request will be sent to the provided callback handler. 516 */ 517 public void performRegistrationRequest( 518 @NonNull RegistrationRequest request, 519 @NonNull RegistrationResponseCallback callback) { 520 checkNotDisposed(); 521 Logger.debug("Initiating dynamic client registration %s", 522 request.configuration.registrationEndpoint.toString()); 523 new RegistrationRequestTask( 524 request, 525 mClientConfiguration.getConnectionBuilder(), 526 callback) 527 .execute(); 528 } 529 530 /** 531 * Disposes state that will not normally be handled by garbage collection. This should be 532 * called when the authorization service is no longer required, including when any owning 533 * activity is paused or destroyed (i.e. in {@link android.app.Activity#onStop()}). 534 */ 535 public void dispose() { 536 if (mDisposed) { 537 return; 538 } 539 mCustomTabManager.dispose(); 540 mDisposed = true; 541 } 542 543 private void checkNotDisposed() { 544 if (mDisposed) { 545 throw new IllegalStateException("Service has been disposed and rendered inoperable"); 546 } 547 } 548 549 private Intent prepareAuthorizationRequestIntent( 550 AuthorizationManagementRequest request, 551 CustomTabsIntent customTabsIntent) { 552 checkNotDisposed(); 553 554 if (mBrowser == null) { 555 throw new ActivityNotFoundException(); 556 } 557 558 Uri requestUri = request.toUri(); 559 Intent intent; 560 if (mBrowser.useCustomTab) { 561 intent = customTabsIntent.intent; 562 } else { 563 intent = new Intent(Intent.ACTION_VIEW); 564 } 565 intent.setPackage(mBrowser.packageName); 566 intent.setData(requestUri); 567 568 Logger.debug("Using %s as browser for auth, custom tab = %s", 569 intent.getPackage(), 570 mBrowser.useCustomTab.toString()); 571 572 //TODO fix logger for configuration 573 //Logger.debug("Initiating authorization request to %s" 574 //request.configuration.authorizationEndpoint); 575 576 return intent; 577 } 578 579 private static class TokenRequestTask 580 extends AsyncTask<Void, Void, JSONObject> { 581 582 private TokenRequest mRequest; 583 private ClientAuthentication mClientAuthentication; 584 private final ConnectionBuilder mConnectionBuilder; 585 private TokenResponseCallback mCallback; 586 private Clock mClock; 587 private boolean mSkipIssuerHttpsCheck; 588 589 private AuthorizationException mException; 590 591 TokenRequestTask(TokenRequest request, 592 @NonNull ClientAuthentication clientAuthentication, 593 @NonNull ConnectionBuilder connectionBuilder, 594 Clock clock, 595 TokenResponseCallback callback, 596 Boolean skipIssuerHttpsCheck) { 597 mRequest = request; 598 mClientAuthentication = clientAuthentication; 599 mConnectionBuilder = connectionBuilder; 600 mClock = clock; 601 mCallback = callback; 602 mSkipIssuerHttpsCheck = skipIssuerHttpsCheck; 603 } 604 605 @Override 606 protected JSONObject doInBackground(Void... voids) { 607 InputStream is = null; 608 try { 609 HttpURLConnection conn = mConnectionBuilder.openConnection( 610 mRequest.configuration.tokenEndpoint); 611 conn.setRequestMethod("POST"); 612 conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); 613 addJsonToAcceptHeader(conn); 614 conn.setDoOutput(true); 615 616 Map<String, String> headers = mClientAuthentication 617 .getRequestHeaders(mRequest.clientId); 618 if (headers != null) { 619 for (Map.Entry<String,String> header : headers.entrySet()) { 620 conn.setRequestProperty(header.getKey(), header.getValue()); 621 } 622 } 623 624 Map<String, String> parameters = mRequest.getRequestParameters(); 625 Map<String, String> clientAuthParams = mClientAuthentication 626 .getRequestParameters(mRequest.clientId); 627 if (clientAuthParams != null) { 628 parameters.putAll(clientAuthParams); 629 } 630 631 String queryData = UriUtil.formUrlEncode(parameters); 632 conn.setRequestProperty("Content-Length", String.valueOf(queryData.length())); 633 OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream()); 634 635 wr.write(queryData); 636 wr.flush(); 637 638 if (conn.getResponseCode() >= HttpURLConnection.HTTP_OK 639 && conn.getResponseCode() < HttpURLConnection.HTTP_MULT_CHOICE) { 640 is = conn.getInputStream(); 641 } else { 642 is = conn.getErrorStream(); 643 } 644 String response = Utils.readInputStream(is); 645 return new JSONObject(response); 646 } catch (IOException ex) { 647 Logger.debugWithStack(ex, "Failed to complete exchange request"); 648 mException = AuthorizationException.fromTemplate( 649 GeneralErrors.NETWORK_ERROR, ex); 650 } catch (JSONException ex) { 651 Logger.debugWithStack(ex, "Failed to complete exchange request"); 652 mException = AuthorizationException.fromTemplate( 653 GeneralErrors.JSON_DESERIALIZATION_ERROR, ex); 654 } finally { 655 Utils.closeQuietly(is); 656 } 657 return null; 658 } 659 660 @Override 661 protected void onPostExecute(JSONObject json) { 662 if (mException != null) { 663 mCallback.onTokenRequestCompleted(null, mException); 664 return; 665 } 666 667 if (json.has(AuthorizationException.PARAM_ERROR)) { 668 AuthorizationException ex; 669 try { 670 String error = json.getString(AuthorizationException.PARAM_ERROR); 671 ex = AuthorizationException.fromOAuthTemplate( 672 TokenRequestErrors.byString(error), 673 error, 674 json.optString(AuthorizationException.PARAM_ERROR_DESCRIPTION, null), 675 UriUtil.parseUriIfAvailable( 676 json.optString(AuthorizationException.PARAM_ERROR_URI))); 677 } catch (JSONException jsonEx) { 678 ex = AuthorizationException.fromTemplate( 679 GeneralErrors.JSON_DESERIALIZATION_ERROR, 680 jsonEx); 681 } 682 mCallback.onTokenRequestCompleted(null, ex); 683 return; 684 } 685 686 TokenResponse response; 687 try { 688 response = new TokenResponse.Builder(mRequest).fromResponseJson(json).build(); 689 } catch (JSONException jsonEx) { 690 mCallback.onTokenRequestCompleted(null, 691 AuthorizationException.fromTemplate( 692 GeneralErrors.JSON_DESERIALIZATION_ERROR, 693 jsonEx)); 694 return; 695 } 696 697 if (response.idToken != null) { 698 IdToken idToken; 699 try { 700 idToken = IdToken.from(response.idToken); 701 } catch (IdTokenException | JSONException ex) { 702 mCallback.onTokenRequestCompleted(null, 703 AuthorizationException.fromTemplate( 704 GeneralErrors.ID_TOKEN_PARSING_ERROR, 705 ex)); 706 return; 707 } 708 709 try { 710 idToken.validate( 711 mRequest, 712 mClock, 713 mSkipIssuerHttpsCheck 714 ); 715 } catch (AuthorizationException ex) { 716 mCallback.onTokenRequestCompleted(null, ex); 717 return; 718 } 719 } 720 Logger.debug("Token exchange with %s completed", 721 mRequest.configuration.tokenEndpoint); 722 mCallback.onTokenRequestCompleted(response, null); 723 } 724 725 /** 726 * GitHub will only return a spec-compliant response if JSON is explicitly defined 727 * as an acceptable response type. As this is essentially harmless for all other 728 * spec-compliant IDPs, we add this header if no existing Accept header has been set 729 * by the connection builder. 730 */ 731 private void addJsonToAcceptHeader(URLConnection conn) { 732 if (TextUtils.isEmpty(conn.getRequestProperty("Accept"))) { 733 conn.setRequestProperty("Accept", "application/json"); 734 } 735 } 736 } 737 738 /** 739 * Callback interface for token endpoint requests. 740 * @see AuthorizationService#performTokenRequest 741 */ 742 public interface TokenResponseCallback { 743 /** 744 * Invoked when the request completes successfully or fails. 745 * 746 * Exactly one of `response` or `ex` will be non-null. If `response` is `null`, a failure 747 * occurred during the request. This can happen if a bad URI was provided, no connection 748 * to the server could be established, or the response JSON was incomplete or incorrectly 749 * formatted. 750 * 751 * @param response the retrieved token response, if successful; `null` otherwise. 752 * @param ex a description of the failure, if one occurred: `null` otherwise. 753 * 754 * @see AuthorizationException.TokenRequestErrors 755 */ 756 void onTokenRequestCompleted(@Nullable TokenResponse response, 757 @Nullable AuthorizationException ex); 758 } 759 760 private static class RegistrationRequestTask 761 extends AsyncTask<Void, Void, JSONObject> { 762 private RegistrationRequest mRequest; 763 private final ConnectionBuilder mConnectionBuilder; 764 private RegistrationResponseCallback mCallback; 765 766 private AuthorizationException mException; 767 768 RegistrationRequestTask(RegistrationRequest request, 769 ConnectionBuilder connectionBuilder, 770 RegistrationResponseCallback callback) { 771 mRequest = request; 772 mConnectionBuilder = connectionBuilder; 773 mCallback = callback; 774 } 775 776 @Override 777 protected JSONObject doInBackground(Void... voids) { 778 InputStream is = null; 779 String postData = mRequest.toJsonString(); 780 try { 781 HttpURLConnection conn = mConnectionBuilder.openConnection( 782 mRequest.configuration.registrationEndpoint); 783 conn.setRequestMethod("POST"); 784 conn.setRequestProperty("Content-Type", "application/json"); 785 conn.setDoOutput(true); 786 conn.setRequestProperty("Content-Length", String.valueOf(postData.length())); 787 OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream()); 788 wr.write(postData); 789 wr.flush(); 790 791 is = conn.getInputStream(); 792 String response = Utils.readInputStream(is); 793 return new JSONObject(response); 794 } catch (IOException ex) { 795 Logger.debugWithStack(ex, "Failed to complete registration request"); 796 mException = AuthorizationException.fromTemplate( 797 GeneralErrors.NETWORK_ERROR, ex); 798 } catch (JSONException ex) { 799 Logger.debugWithStack(ex, "Failed to complete registration request"); 800 mException = AuthorizationException.fromTemplate( 801 GeneralErrors.JSON_DESERIALIZATION_ERROR, ex); 802 } finally { 803 Utils.closeQuietly(is); 804 } 805 return null; 806 } 807 808 @Override 809 protected void onPostExecute(JSONObject json) { 810 if (mException != null) { 811 mCallback.onRegistrationRequestCompleted(null, mException); 812 return; 813 } 814 815 if (json.has(AuthorizationException.PARAM_ERROR)) { 816 AuthorizationException ex; 817 try { 818 String error = json.getString(AuthorizationException.PARAM_ERROR); 819 ex = AuthorizationException.fromOAuthTemplate( 820 RegistrationRequestErrors.byString(error), 821 error, 822 json.getString(AuthorizationException.PARAM_ERROR_DESCRIPTION), 823 UriUtil.parseUriIfAvailable( 824 json.getString(AuthorizationException.PARAM_ERROR_URI))); 825 } catch (JSONException jsonEx) { 826 ex = AuthorizationException.fromTemplate( 827 GeneralErrors.JSON_DESERIALIZATION_ERROR, 828 jsonEx); 829 } 830 mCallback.onRegistrationRequestCompleted(null, ex); 831 return; 832 } 833 834 RegistrationResponse response; 835 try { 836 response = new RegistrationResponse.Builder(mRequest) 837 .fromResponseJson(json).build(); 838 } catch (JSONException jsonEx) { 839 mCallback.onRegistrationRequestCompleted(null, 840 AuthorizationException.fromTemplate( 841 GeneralErrors.JSON_DESERIALIZATION_ERROR, 842 jsonEx)); 843 return; 844 } catch (RegistrationResponse.MissingArgumentException ex) { 845 Logger.errorWithStack(ex, "Malformed registration response"); 846 mException = AuthorizationException.fromTemplate( 847 GeneralErrors.INVALID_REGISTRATION_RESPONSE, 848 ex); 849 return; 850 } 851 Logger.debug("Dynamic registration with %s completed", 852 mRequest.configuration.registrationEndpoint); 853 mCallback.onRegistrationRequestCompleted(response, null); 854 } 855 } 856 857 /** 858 * Callback interface for token endpoint requests. 859 * 860 * @see AuthorizationService#performTokenRequest 861 */ 862 public interface RegistrationResponseCallback { 863 /** 864 * Invoked when the request completes successfully or fails. 865 * 866 * Exactly one of `response` or `ex` will be non-null. If `response` is `null`, a failure 867 * occurred during the request. This can happen if an invalid URI was provided, no 868 * connection to the server could be established, or the response JSON was incomplete or 869 * incorrectly formatted. 870 * 871 * @param response the retrieved registration response, if successful; `null` otherwise. 872 * @param ex a description of the failure, if one occurred: `null` otherwise. 873 * @see AuthorizationException.RegistrationRequestErrors 874 */ 875 void onRegistrationRequestCompleted(@Nullable RegistrationResponse response, 876 @Nullable AuthorizationException ex); 877 } 878}