Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CCT - AuthTab Sample Demo #486

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ buildscript {
repositories {
google()
jcenter()
maven {
url = uri("https://androidx.dev/snapshots/builds/12502757/artifacts/repository")
}
}
dependencies {
classpath 'com.android.tools.build:gradle:8.7.3'
Expand All @@ -31,6 +34,12 @@ allprojects {
google()
jcenter()

// AndroidX snapshots repository
maven {
url = "https://androidx.dev/snapshots/builds/12815573/artifacts/repository"
}

// Repository for DexMaker
maven {
url = "https://linkedin.jfrog.io/artifactory/open-source/"
content {
Expand Down
1 change: 1 addition & 0 deletions demos/custom-tabs-auth-tab/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
51 changes: 51 additions & 0 deletions demos/custom-tabs-auth-tab/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

apply plugin: 'com.android.application'

android {
namespace 'com.google.androidbrowserhelper.demos.customtabsauthtab'
compileSdkVersion 35
defaultConfig {
applicationId "com.google.androidbrowserhelper.demos.customtabsauthtab"
minSdkVersion 26
targetSdkVersion 35
versionCode 1
versionName "1.0"
}

buildTypes {
release {
minifyEnabled false
}
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}

dependencies {
implementation project(path: ':androidbrowserhelper')
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.activity:activity:1.9.3'
implementation 'androidx.browser:browser:1.9.0-SNAPSHOT'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.annotation:annotation:1.9.1'
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
}
35 changes: 35 additions & 0 deletions demos/custom-tabs-auth-tab/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.google.androidbrowserhelper.demos.customtabsauthtab">
<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AuthTab"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<!-- An intent-filter for fallback to Chrome Custom Tabs -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="auth" />
<data android:host="callback" />
</intent-filter>
</activity>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.google.androidbrowserhelper.demos.customtabsauthview;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
import androidx.browser.auth.AuthTabIntent;
import androidx.annotation.OptIn;
import androidx.browser.auth.ExperimentalAuthTab;

import java.io.IOException;
import java.util.UUID;

/**
* This class helps managing an authentication flow. It was created with the goal of demonstrating
* how to use Custom Tabs Auth Tab to handle auth and is not meant as a complete implementation
* of the OAuth protocol. We recommend checking out https://github.com/openid/AppAuth-Android for
* a comprehensive implementation of the OAuth protocol.
*/

@OptIn(markerClass = ExperimentalAuthTab.class)
public class AuthManager {
private static final String TAG = "OAuthManager";

private final String mClientId;
private final String mClientSecret;
private final String mAuthorizationEndpoint;
private final String mRedirectScheme;

public interface OAuthCallback {
void auth(String accessToken, String scope, String tokenType);
}

public AuthManager(String clientId, String clientSecret, String authorizationEndpoint,
String redirectScheme) {
mClientId = clientId;
mClientSecret = clientSecret;
mAuthorizationEndpoint = authorizationEndpoint;
mRedirectScheme = redirectScheme;
}

public void authorize(Context context, ActivityResultLauncher<Intent> launcher, String scope) {
// Generate a random state.
String state = UUID.randomUUID().toString();

// Save the state so we can verify later.
SharedPreferences preferences =
context.getSharedPreferences("OAUTH_STORAGE", Context.MODE_PRIVATE);
preferences.edit()
.putString("OAUTH_STATE", state)
.apply();

// Create an authorization URI to the OAuth Endpoint.
Uri uri = Uri.parse(mAuthorizationEndpoint)
.buildUpon()
.appendQueryParameter("response_type", "code")
.appendQueryParameter("client_id", mClientId)
.appendQueryParameter("scope", scope)
.appendQueryParameter("state", state)
.build();

// Open the Authorization URI in a Chrome Custom Auth Tab.
AuthTabIntent authTabIntent = new AuthTabIntent.Builder().build();
authTabIntent.launch(launcher, uri, mRedirectScheme);
}

public void continueAuthFlow(@NonNull Context context, Uri uri, @NonNull OAuthCallback callback) {
String code = uri.getQueryParameter("code");
SharedPreferences preferences =
context.getSharedPreferences("OAUTH_STORAGE", Context.MODE_PRIVATE);
String state = preferences.getString("OAUTH_STATE", "");
Uri tokenUri = Uri.parse("https://github.com/login/oauth/access_token")
.buildUpon()
.appendQueryParameter("client_id", mClientId)
.appendQueryParameter("client_secret", mClientSecret)
.appendQueryParameter("code", code)
.appendQueryParameter("state", state)
.build();

// Run the network request off the UI thread.
new Thread(() -> {
try {
String response = Utils.fetch(tokenUri);
// The response is a query-string. We concatenate with a valid domain to be
// able to easily parse and extract values.
Uri responseUri = Uri.parse("http://example.com?" + response);
String accessToken = responseUri.getQueryParameter("access_token");
String tokenType = responseUri.getQueryParameter("token_type");
String scope = responseUri.getQueryParameter("scope");

// Invoke the callback in the main thread.
new Handler(Looper.getMainLooper()).post(
() -> callback.auth(accessToken, scope, tokenType));

} catch (IOException e) {
Log.e(TAG, "Error requesting access token: " + e.getMessage());
}
}).start();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.google.androidbrowserhelper.demos.customtabsauthview;

import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.util.Collections;
import java.util.Map;

public class GithubApi {
private static final String TAG = "GithubAPI";
private static final String API_ENDPOINT = "https://api.github.com/user";
private static final String AUTH_HEADER_KEY = "Authorization";

public interface UserCallback {
void onUserData(String username);
}

public static void requestGithubUsername(String token, UserCallback callback) {
new Thread(() -> {
try {
Uri uri = Uri.parse(API_ENDPOINT);
Map<String, String> headers =
Collections.singletonMap(AUTH_HEADER_KEY, "token " + token);
String response = Utils.fetch(uri, headers);
JSONObject user = new JSONObject(response);
String username = user.getString("name");

// Invoke the callback in the main thread.
new Handler(Looper.getMainLooper()).post(() -> {
callback.onUserData(username);
});
} catch (IOException | JSONException ex) {
Log.e(TAG, "Error fetching GitHub user: " + ex.getMessage());
}
}).start();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.androidbrowserhelper.demos.customtabsauthview;

import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;

import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.OptIn;
import androidx.appcompat.app.AppCompatActivity;
import androidx.browser.auth.AuthTabIntent;
import androidx.browser.auth.ExperimentalAuthTab;

@OptIn(markerClass = ExperimentalAuthTab.class)
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";

private static final String AUTHORIZATION_ENDPOINT = "https://github.com/login/oauth/authorize";
private static final String CLIENT_ID = "<github-client-id>";
private static final String CLIENT_SECRET = "<github-client-secret>";
private static final String REDIRECT_SCHEME = "auth";

private static final AuthManager O_AUTH_MANAGER =
new AuthManager(CLIENT_ID, CLIENT_SECRET, AUTHORIZATION_ENDPOINT, REDIRECT_SCHEME);

private final ActivityResultLauncher<Intent> mLauncher =
AuthTabIntent.registerActivityResultLauncher(this, this::handleAuthResult);

private Button mLoginButton;
private TextView mUserText;
private ProgressBar mProgressBar;
private boolean mLoggedIn;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

mLoginButton = findViewById(R.id.login_button);
mUserText = findViewById(R.id.user_text);
mProgressBar = findViewById(R.id.progress_bar);

Intent intent = getIntent();
if (intent != null) {
Uri data = intent.getData();
if (data != null && data.getHost() != null
&& data.getHost().startsWith("callback")) {
mProgressBar.setVisibility(View.VISIBLE);
mLoginButton.setEnabled(false);
completeAuth(data);
}
}
}

public void login(View v) {
if (mLoggedIn) {
mLoginButton.setText(R.string.login);
mUserText.setText(R.string.logged_out);
mLoggedIn = false;
} else {
O_AUTH_MANAGER.authorize(this, mLauncher, "user");
}
}

private void handleAuthResult(AuthTabIntent.AuthResult result) {
if (result.resultCode == AuthTabIntent.RESULT_OK) {
completeAuth(result.resultUri);
}
}

private void completeAuth(Uri uri) {
O_AUTH_MANAGER.continueAuthFlow(this, uri, (accessToken, scope, tokenType) -> {
GithubApi.requestGithubUsername(accessToken, (username -> {
mLoginButton.setText(R.string.logout);
mLoginButton.setEnabled(true);
mProgressBar.setVisibility(View.INVISIBLE);
mUserText.setText(getString(R.string.logged_in, username));
mLoggedIn = true;
}));
});
}
}
Loading
Loading