Skip to content
Closed
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
29 changes: 29 additions & 0 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# This workflow will build a Java project with Gradle
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle

name: Java CI with Gradle

on:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+'

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew build
- uses: actions/upload-artifact@v2
with:
name: Package
path: build/libs
82 changes: 77 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
![AppAuth for Android](https://rawgit.com/openid/AppAuth-Android/master/appauth_lockup.svg)

[![Download](https://api.bintray.com/packages/openid/net.openid/appauth/images/download.svg) ](https://bintray.com/openid/net.openid/appauth/_latestVersion)
[![Javadocs](http://javadoc.io/badge/net.openid/appauth.svg)](http://javadoc.io/doc/net.openid/appauth)
[![Build Status](https://travis-ci.org/openid/AppAuth-Android.svg?branch=master)](https://travis-ci.org/openid/AppAuth-Android)
[![](https://jitpack.io/v/trufla-technology/AppAuth-Android.svg)](https://jitpack.io/#trufla-technology/AppAuth-Android)
[![Download](https://api.bintray.com/packages/trufla-technology/net.openid/appauth/images/download.svg) ](https://bintray.com/openid/net.openid/appauth/_latestVersion)
[![Javadocs](http://javadoc.io/badge/net.trufla-technology/appauth.svg)](http://javadoc.io/doc/net.trufla-technology/appauth)
[![Build Status](https://travis-ci.org/trufla-technology/AppAuth-Android.svg?branch=master)](https://travis-ci.org/openid/AppAuth-Android)
[![Codacy Badge](https://api.codacy.com/project/badge/grade/321412eec811478085ec6c4c923ad8a1)](https://www.codacy.com/app/iainmcgin/AppAuth-Android)
[![codecov.io](https://codecov.io/github/openid/AppAuth-Android/coverage.svg?branch=master)](https://codecov.io/github/openid/AppAuth-Android?branch=master)
[![codecov.io](https://codecov.io/github/trufla-technology/AppAuth-Android/coverage.svg?branch=master)](https://codecov.io/github/openid/AppAuth-Android?branch=master)

AppAuth for Android is a client SDK for communicating with
[OAuth 2.0](https://tools.ietf.org/html/rfc6749) and
Expand Down Expand Up @@ -440,6 +440,78 @@ authState.performActionWithFreshTokens(service, new AuthStateAction() {
});
```

### Ending current session (Draft)

Given you have a logged in session and you want to end it. In that case you need to get:
- `AuthorizationServiceConfiguration`
- valid Open Id Token that you should get after authentication
- End of session URI that should be provided within you OpenId service config

First you have to build EndSessionRequest

```java
EndSessionRequest endSessionRequest =
new EndSessionRequest.Builder(
authorizationServiceConfiguration,
idToken,
endSessionRedirectUri
).build();
```
This request can then be dispatched using one of two approaches.

a `startActivityForResult` call using an Intent returned from the `AuthorizationService`,
or by calling `performEndSessionRequest` and providing pending intent for completion
and cancelation handling activities.

The startActivityForResult approach is simpler to use but may require more processing of the result:

```java
private void endSession() {
AuthorizationService authService = new AuthorizationService(this);
Intent endSessionItent = authService.getEndSessionRequestIntent(endSessionRequest);
startActivityForResult(endSessionItent, RC_END_SESSION);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == RC_END_SESSION) {
EndSessionResonse resp = EndSessionResonse.fromIntent(data);
AuthorizationException ex = AuthorizationException.fromIntent(data);
// ... process the response or exception ...
} else {
// ...
}
}
```
If instead you wish to directly transition to another activity on completion or cancelation,
you can use `performEndSessionRequest`:

```java
AuthorizationService authService = new AuthorizationService(this);

authService.performEndSessionRequest(
endSessionRequest,
PendingIntent.getActivity(this, 0, new Intent(this, MyAuthCompleteActivity.class), 0),
PendingIntent.getActivity(this, 0, new Intent(this, MyAuthCanceledActivity.class), 0));
```

End session flow will also work involving browser mechanism that is described in authorization
mechanism session.
Handling response mechanism with transition to another activity should be as follows:

```java
public void onCreate(Bundle b) {
EndSessionResponse resp = EndSessionResponse.fromIntent(getIntent());
AuthorizationException ex = AuthorizationException.fromIntent(getIntent());
if (resp != null) {
// authorization completed
} else {
// authorization failed, check ex for more details
}
// ...
}
```

### AuthState persistence

Instances of `AuthState` keep track of the authorization and token
Expand Down
4 changes: 3 additions & 1 deletion app/README-Okta.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ You can create an Okta developer account at [https://developer.okta.com/](https:
| Setting | Value |
| ------------------- | --------------------------------------------------- |
| Application Name | OpenId Connect App *(must be unique)* |
| Redirect URIs | com.oktapreview.yoursubdomain://callback_url|
| Login redirect URIs | com.oktapreview.yoursubdomain://callback_url|
| Logout redirect URIs| com.oktapreview.yoursubdomain://callback_url|
| Allowed grant types | Authorization Code |

1. Click **Finish** to redirect back to the *General Settings* of your application.
Expand All @@ -27,6 +28,7 @@ You can create an Okta developer account at [https://developer.okta.com/](https:
{
"client_id": "{{YourClientID}}",
"redirect_uri": "com.oktapreview.{{yourOrg}}:/oauth",
"end_session_uri": "com.oktapreview.{{yourOrg}}:/{logoutCallback}",
"authorization_scope": "openid email profile",
"discovery_uri": "https://{{yourOrg}}.okta.com/.well-known/openid-configuration"
}
Expand Down
9 changes: 9 additions & 0 deletions app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ The configuration file MUST contain a JSON object. The following properties can
The value specified here should match the value specified for `appAuthRedirectScheme` in the
`build.gradle` (Module: app), so that the demo app can capture the response.

- `end_sesion_uri` (required): The redirect URI to use for receiving the end session response.
This should be a custom scheme URI (com.example.app:/oauth2redirect/example-provider).
Consult the documentation for your authorization server.

The value specified here should match the value specified for `appAuthRedirectScheme` in the
`build.gradle` (Module: app), so that the demo app can capture the response.

NOTE: Scheme of the URI should be the same as `redirect_uri` but callback should be different.

- `authorization_scope` (required): The scope string to use for the authorization request.
For the purposes of the demo, we recommend the value "openid profile email", though any value
understood by your authorization server can be used.
Expand Down
14 changes: 14 additions & 0 deletions app/java/net/openid/appauthdemo/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,11 @@ public final class Configuration {
private String mClientId;
private String mScope;
private Uri mRedirectUri;
private Uri mEndSessionUri;
private Uri mDiscoveryUri;
private Uri mAuthEndpointUri;
private Uri mTokenEndpointUri;
private Uri mEndSessionEndpoint;
private Uri mRegistrationEndpointUri;
private Uri mUserInfoEndpointUri;
private boolean mHttpsRequired;
Expand Down Expand Up @@ -141,6 +143,11 @@ public Uri getDiscoveryUri() {
return mDiscoveryUri;
}

@Nullable
public Uri getEndSessionUri() {
return mEndSessionUri;
}

@Nullable
public Uri getAuthEndpointUri() {
return mAuthEndpointUri;
Expand All @@ -151,6 +158,11 @@ public Uri getTokenEndpointUri() {
return mTokenEndpointUri;
}

@Nullable
public Uri getEndSessionEndpoint() {
return mEndSessionEndpoint;
}

@Nullable
public Uri getRegistrationEndpointUri() {
return mRegistrationEndpointUri;
Expand Down Expand Up @@ -195,6 +207,7 @@ private void readConfiguration() throws InvalidConfigurationException {
mClientId = getConfigString("client_id");
mScope = getRequiredConfigString("authorization_scope");
mRedirectUri = getRequiredConfigUri("redirect_uri");
mEndSessionUri = getRequiredConfigUri("end_session_uri");

if (!isRedirectUriRegistered()) {
throw new InvalidConfigurationException(
Expand All @@ -209,6 +222,7 @@ private void readConfiguration() throws InvalidConfigurationException {

mTokenEndpointUri = getRequiredConfigWebUri("token_endpoint_uri");
mUserInfoEndpointUri = getRequiredConfigWebUri("user_info_endpoint_uri");
mEndSessionEndpoint = getRequiredConfigUri("end_session_endpoint");

if (mClientId == null) {
mRegistrationEndpointUri = getRequiredConfigWebUri("registration_endpoint_uri");
Expand Down
1 change: 1 addition & 0 deletions app/java/net/openid/appauthdemo/LoginActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ private void initializeAppAuth() {
AuthorizationServiceConfiguration config = new AuthorizationServiceConfiguration(
mConfiguration.getAuthEndpointUri(),
mConfiguration.getTokenEndpointUri(),
mConfiguration.getEndSessionEndpoint(),
mConfiguration.getRegistrationEndpointUri());

mAuthStateManager.replace(new AuthState(config));
Expand Down
118 changes: 99 additions & 19 deletions app/java/net/openid/appauthdemo/TokenActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

package net.openid.appauthdemo;

import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
Expand All @@ -36,6 +38,9 @@
import net.openid.appauth.AuthorizationService;
import net.openid.appauth.AuthorizationServiceDiscovery;
import net.openid.appauth.ClientAuthentication;
import net.openid.appauth.EndSessionRequest;
import net.openid.appauth.LogoutRequest;
import net.openid.appauth.LogoutService;
import net.openid.appauth.TokenRequest;
import net.openid.appauth.TokenResponse;
import okio.Okio;
Expand Down Expand Up @@ -65,6 +70,9 @@ public class TokenActivity extends AppCompatActivity {

private static final String KEY_USER_INFO = "userInfo";

private static final int END_SESSION_REQUEST_CODE = 911;
private static final String EXTRA_AUTH_SERVICE_DISCOVERY = "authServiceDiscovery";

private AuthorizationService mAuthService;
private AuthStateManager mStateManager;
private final AtomicReference<JSONObject> mUserInfoJson = new AtomicReference<>();
Expand Down Expand Up @@ -186,7 +194,7 @@ private void displayAuthorized() {

AuthState state = mStateManager.getCurrent();

TextView refreshTokenInfoView = (TextView) findViewById(R.id.refresh_token_info);
TextView refreshTokenInfoView = findViewById(R.id.refresh_token_info);
refreshTokenInfoView.setText((state.getRefreshToken() == null)
? R.string.no_refresh_token_returned
: R.string.refresh_token_returned);
Expand Down Expand Up @@ -230,7 +238,7 @@ private void displayAuthorized() {
viewProfileButton.setOnClickListener((View view) -> fetchUserInfo());
}

((Button)findViewById(R.id.sign_out)).setOnClickListener((View view) -> signOut());
findViewById(R.id.sign_out).setOnClickListener((View view) -> endSession());

View userInfoCard = findViewById(R.id.userinfo_card);
JSONObject userInfo = mUserInfoJson.get();
Expand Down Expand Up @@ -321,11 +329,7 @@ private void handleCodeExchangeResponse(
}
}

/**
* Demonstrates the use of {@link AuthState#performActionWithFreshTokens} to retrieve
* user info from the IDP's user info endpoint. This callback will negotiate a new access
* token / id token for use in a follow-up action, or provide an error if this fails.
*/

@MainThread
private void fetchUserInfo() {
displayLoading("Fetching user info");
Expand Down Expand Up @@ -380,6 +384,24 @@ private void fetchUserInfo(String accessToken, String idToken, AuthorizationExce
});
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == END_SESSION_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
signOut();
finish();
} else {
displayEndSessionCancelled();
}
}

private void displayEndSessionCancelled() {
Snackbar.make(findViewById(R.id.coordinator),
"Sign out canceled",
Snackbar.LENGTH_SHORT)
.show();
}

@MainThread
private void showSnackbar(String message) {
Snackbar.make(findViewById(R.id.coordinator),
Expand All @@ -388,21 +410,79 @@ private void showSnackbar(String message) {
.show();
}

@MainThread
private void endSession() {
Intent endSessionEnten = mAuthService.getEndSessionRequestIntent(
new EndSessionRequest.Builder(
mStateManager.getCurrent().getAuthorizationServiceConfiguration(),
mStateManager.getCurrent().getIdToken(),
mConfiguration.getEndSessionUri()).build());
startActivityForResult(endSessionEnten, END_SESSION_REQUEST_CODE);
}


static AuthorizationServiceDiscovery getDiscoveryDocFromIntent(Intent intent) {
if (!intent.hasExtra(EXTRA_AUTH_SERVICE_DISCOVERY)) {
return null;
}
String discoveryJson = intent.getStringExtra(EXTRA_AUTH_SERVICE_DISCOVERY);
try {
return new AuthorizationServiceDiscovery(new JSONObject(discoveryJson));
} catch (JSONException | AuthorizationServiceDiscovery.MissingArgumentException ex) {
throw new IllegalStateException("Malformed JSON in discovery doc");
}
}

@MainThread
private void signOut() {
// discard the authorization and token state, but retain the configuration and
// dynamic client registration (if applicable), to save from retrieving them again.
AuthState currentState = mStateManager.getCurrent();
AuthState clearedState =
new AuthState(currentState.getAuthorizationServiceConfiguration());
if (currentState.getLastRegistrationResponse() != null) {
clearedState.update(currentState.getLastRegistrationResponse());
// // discard the authorization and token state, but retain the configuration and
// // dynamic client registration (if applicable), to save from retrieving them again.
// AuthState currentState = mStateManager.getCurrent();
// AuthState clearedState =
// new AuthState(currentState.getAuthorizationServiceConfiguration());
// if (currentState.getLastRegistrationResponse() != null) {
// clearedState.update(currentState.getLastRegistrationResponse());
// }
// mStateManager.replace(clearedState);
//
// Intent mainIntent = new Intent(this, LoginActivity.class);
// mainIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
// startActivity(mainIntent);
// finish();

AuthState mAuthState = mStateManager.getCurrent();

if (mAuthState.getAuthorizationServiceConfiguration() == null) {
Log.e(TAG, "Cannot make userInfo request without service configuration");
}
mStateManager.replace(clearedState);

Intent mainIntent = new Intent(this, LoginActivity.class);
mainIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(mainIntent);
finish();
mAuthState.performActionWithFreshTokens(mAuthService, new AuthState.AuthStateAction() {
@Override
public void execute(String accessToken, String idToken, AuthorizationException ex) {
if (ex != null) {
Log.e(TAG, "Token refresh failed when fetching user info");
return;
}

AuthorizationServiceDiscovery discoveryDoc = getDiscoveryDocFromIntent(getIntent());
if (discoveryDoc == null) {
throw new IllegalStateException("no available discovery doc");
}

Uri endSessionEndpoint = Uri.parse(discoveryDoc.getEndSessionEndpoint().toString());

String logoutUri = getResources().getString(R.string.keycloak_auth_logout_uri);
LogoutRequest logoutRequest = new LogoutRequest(endSessionEndpoint,
Uri.parse(logoutUri));

LogoutService logoutService = new LogoutService(TokenActivity.this);
logoutService.performLogoutRequest(
logoutRequest,
PendingIntent.getActivity(
TokenActivity.this, logoutRequest.hashCode(),
new Intent(TokenActivity.this, LoginActivity.class), 0)
);
}
});
}
}
Loading