Skip to content

Commit

Permalink
Merge pull request #108 from uber/ts/chrometab
Browse files Browse the repository at this point in the history
  • Loading branch information
tyvsmith authored Feb 9, 2018
2 parents 4d77c38 + 340dca0 commit 1c08359
Show file tree
Hide file tree
Showing 24 changed files with 1,155 additions and 141 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
v0.8.0 - TBD
------------

### Changed
- [Issue #101](https://github.com/uber/rides-android-sdk/issues/101) LoginManager now uses AccessTokenStorage

### Added
- [Issue #22](https://github.com/uber/rides-android-sdk/issues/22) Customtab support


v0.7.0 - 11/17/2017
------------

Expand Down
82 changes: 80 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,14 +157,26 @@ If you want to provide a more custom experience in your app, there are a few cla
### Login
The Uber SDK allows for three login flows: Implicit Grant (local web view), Single Sign On with the Uber App, and Authorization Code Grant (requires a backend to catch the local web view redirect and complete OAuth).

To use Single Sign On you must register a hash of your application's signing certificate in the Application Signature section of the [developer dashboard](https://developer.uber.com/dashboard).

#### Dashboard configuration
To use SDK features, two configuration details must be set on the Uber Developer Dashboard.

1. Sign into to the [developer dashboard](https://developer.uber.com/dashboard)

1. Register a redirect URI to be used to communication authentication results. The default used
by the SDK is in the format of `applicationId.uberauth://redirect`. ex: `com.example
.uberauth://redirect`. To configure the SDK to use a different redirect URI, see the steps below.

1. To use Single Sign On you must register a hash of your application's signing certificate in the
Application Signature section of the settings page of your application.

To get the hash of your signing certificate, run this command with the alias of your key and path to your keystore:

```sh
keytool -exportcert -alias <your_key_alias> -keystore <your_keystore_path> | openssl sha1 -binary | openssl base64
```


Before you can request any rides, you need to get an `AccessToken`. The Uber Rides SDK provides the `LoginManager` class for this task. Simply create a new instance and use its login method to present the login screen to the user.

```java
Expand Down Expand Up @@ -199,7 +211,73 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data){
}
```

The default behavior of calling `LoginManager.login(activity)` is to activate Single Sign On, and if that is unavailable, fallback to Implicit Grant if privileged scopes are not requested, otherwise redirect to the Play Store. If Authorization Code Grant is required, set `LoginManager.setRedirectForAuthorizationCode(true)` to prevent the redirect to the Play Store. Implicit Grant will allow access to all non-privileged scopes, where as the other two both grant access to privileged scopes. [Read more about scopes](https://developer.uber.com/docs/scopes).
#### Authentication Migration and setup (Version 0.8 and above)
With Version 0.8 and above of the SDK, the redirect URI is more strongly enforced to meet IETF
standards [IETF RFC](https://tools.ietf.org/html/draft-ietf-oauth-native-apps-12).

The SDK will automatically created a redirect URI to be used in the oauth callbacks with
the format "applicationId.uberauth", ex "com.example.uberauth". **This URI must be registered in
the [developer dashboard](https://developer.uber.com/dashboard)**

If this differs from the previous specified redirect URI configured in the SessionConfiguration,
there are a few options.

1. Change the redirect URI to match the new scheme in the configuration of the Session. If this
is left out entirely, the default will be used.

```java
SessionConfiguration config = new SessionConfiguration.Builder()
.setRedirectUri("com.example.app.uberauth")
.build();
```

2. Override the LoginRedirectReceiverActivity in your main manifest and provide a custom intent
filter. Register this custom URI in the developer dashboard for your application.

```xml
<activity
android:name="com.uber.sdk.android.core.auth.LoginRedirectReceiverActivity"
tools:node="replace">
<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:scheme="com.example.app"
android:host="redirect" />
</intent-filter>
</activity>
```

3. If using [Authorization Code Flow](https://developer.uber.com/docs/riders/guides/authentication/user-access-token), you will need to configure your server to redirect to
the Mobile Application with an access token either via the generated URI or a custom URI as defined in steps 1 and 2.

The Session should be configured to redirect to the server to do a code exchange and the login
manager should indicate the SDK is operating in the Authorization Code Flow.

```java
SessionConfiguration config = new SessionConfiguration.Builder()
.setRedirectUri("example.com/redirect") //Where this is your configured server
.build();

loginManager.setAuthCodeEnabled(true);
loginManager.login(this);

```

Once the code is exchanged, the server should redirect to a URI in the standard OAUTH format of
`com.example.uberauth://redirect#access_token=ACCESS_TOKEN&token_type=Bearer&expires_in=TTL&scope=SCOPES`
for the SDK to receive the access token and continue operation.``


##### Authorization Code Flow


The default behavior of calling `LoginManager.login(activity)` is to activate Single Sign On,
and if SSO is unavailable, fallback to Implicit Grant if privileged scopes are not requested,
otherwise redirect to the Play Store. If Authorization Code Grant is required, set `LoginManager
.setAuthCodeEnabled(true)` to prevent the redirect to the Play Store. Implicit Grant will allow
access to all non-privileged scopes, where as the other two both grant access to privileged scopes. [Read more about scopes](https://developer.uber.com/docs/scopes).


#### Login Errors
Upon a failure to login, an `AuthenticationError` will be provided in the `LoginCallback`. This enum provides a series of values that provide more information on the type of error.
Expand Down
1 change: 1 addition & 0 deletions core-android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ dependencies {
compile (deps.uber.uberCore) {
exclude module: 'slf4j-log4j12'
}
compile deps.support.chrometabs
compile deps.misc.jsr305
compile deps.support.appCompat
compile deps.support.annotations
Expand Down
15 changes: 14 additions & 1 deletion core-android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@
<application>
<activity android:name=".auth.LoginActivity"
android:exported="false"
android:screenOrientation="portrait"/>
android:screenOrientation="portrait"
android:launchMode="singleTask">
</activity>

<activity android:name=".auth.LoginRedirectReceiverActivity"
android:exported="true">
<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:scheme="${applicationId}.uberauth"
android:host="redirect" />
</intent-filter>
</activity>
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,27 @@

package com.uber.sdk.android.core.auth;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Base64;
import android.webkit.WebView;

import com.uber.sdk.core.auth.AccessToken;
import com.uber.sdk.core.auth.Scope;
import com.uber.sdk.core.client.SessionConfiguration;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;

/**
Expand Down Expand Up @@ -118,6 +128,20 @@ static Collection<Scope> stringCollectionToScopeCollection(@NonNull Collection<S
return scopeCollection;
}


public static boolean isRedirectUriRegistered(@NonNull Activity activity, @NonNull Uri uri) {

Intent intent = new Intent(Intent.ACTION_VIEW, uri);
intent.setAction(Intent.ACTION_VIEW);
intent.setData(uri);
ComponentName info = intent.resolveActivity(activity.getPackageManager());

return info != null && info.getClassName().equals(LoginRedirectReceiverActivity.class
.getName());


}

/**
* Converts a {@link Collection} of {@link Scope}s into a space-delimited {@link String}.
*
Expand Down Expand Up @@ -150,7 +174,29 @@ public static String mergeScopeStrings(String... scopes) {
}

@NonNull
static Intent parseTokenUri(@NonNull Uri uri) throws LoginAuthenticationException {
static AccessToken parseTokenUri(@NonNull Uri uri) throws LoginAuthenticationException {
final long expiresIn;
try {
expiresIn = Long.valueOf(uri.getQueryParameter(KEY_EXPIRATION_TIME));
} catch (NumberFormatException ex) {
throw new LoginAuthenticationException(AuthenticationError.INVALID_RESPONSE);
}

final String accessToken = uri.getQueryParameter(KEY_TOKEN);
final String refreshToken = uri.getQueryParameter(KEY_REFRESH_TOKEN);
final String scope = uri.getQueryParameter(KEY_SCOPES);
final String tokenType = uri.getQueryParameter(KEY_TOKEN_TYPE);

if (TextUtils.isEmpty(accessToken) || TextUtils.isEmpty(scope) || TextUtils.isEmpty(tokenType)) {
throw new LoginAuthenticationException(AuthenticationError.INVALID_RESPONSE);
}

return new AccessToken(expiresIn, AuthUtils.stringToScopeCollection
(scope), accessToken, refreshToken, tokenType);
}

@NonNull
static Intent parseTokenUriToIntent(@NonNull Uri uri) throws LoginAuthenticationException {
final long expiresIn;
try {
expiresIn = Long.valueOf(uri.getQueryParameter(KEY_EXPIRATION_TIME));
Expand Down Expand Up @@ -201,4 +247,52 @@ static AccessToken createAccessToken(Intent intent) {
static String createEncodedParam(String rawParam) {
return Base64.encodeToString(rawParam.getBytes(), Base64.DEFAULT);
}

/**
* Builds a URL {@link String} using the necessary parameters to load in the {@link WebView}.
*
* @return the URL to load in the {@link WebView}
*/
@NonNull
static String buildUrl(
@NonNull String redirectUri,
@NonNull ResponseType responseType,
@NonNull SessionConfiguration configuration) {

final String CLIENT_ID_PARAM = "client_id";
final String ENDPOINT = "login";
final String HTTPS = "https";
final String PATH = "oauth/v2/authorize";
final String REDIRECT_PARAM = "redirect_uri";
final String RESPONSE_TYPE_PARAM = "response_type";
final String SCOPE_PARAM = "scope";
final String SHOW_FB_PARAM = "show_fb";
final String SIGNUP_PARAMS = "signup_params";
final String REDIRECT_LOGIN = "{\"redirect_to_login\":true}";



Uri.Builder builder = new Uri.Builder();
builder.scheme(HTTPS)
.authority(ENDPOINT + "." + configuration.getEndpointRegion().getDomain())
.appendEncodedPath(PATH)
.appendQueryParameter(CLIENT_ID_PARAM, configuration.getClientId())
.appendQueryParameter(REDIRECT_PARAM, redirectUri)
.appendQueryParameter(RESPONSE_TYPE_PARAM, responseType.toString().toLowerCase(
Locale.US))
.appendQueryParameter(SCOPE_PARAM, getScopes(configuration))
.appendQueryParameter(SHOW_FB_PARAM, "false")
.appendQueryParameter(SIGNUP_PARAMS, AuthUtils.createEncodedParam(REDIRECT_LOGIN));

return builder.build().toString();
}

private static String getScopes(SessionConfiguration configuration) {
String scopes = AuthUtils.scopeCollectionToString(configuration.getScopes());
if (!configuration.getCustomScopes().isEmpty()) {
scopes = AuthUtils.mergeScopeStrings(scopes,
AuthUtils.customScopeCollectionToString(configuration.getCustomScopes()));
}
return scopes;
}
}
Loading

0 comments on commit 1c08359

Please sign in to comment.