Skip to content

Commit

Permalink
Merge pull request #45 from spt-identity/backwards-compat-session-sto…
Browse files Browse the repository at this point in the history
…rage

Backwards compat session storage
  • Loading branch information
astri-magnussen authored and GitHub Enterprise committed Oct 7, 2021
2 parents 22f3e86 + e30a0b6 commit ba58110
Show file tree
Hide file tree
Showing 40 changed files with 2,165 additions and 297 deletions.
108 changes: 76 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,27 @@
New implementation of the Schibsted account Android SDK using the web flows via
[Custom Tabs](https://developer.chrome.com/docs/android/custom-tabs/overview/).

API documentation can be found [here](https://pages.github.schibsted.io/spt-identity/account-sdk-android-web/).
API documentation can be
found [here](https://pages.github.schibsted.io/spt-identity/account-sdk-android-web/).

Git repository includes a example implementation of the sdk, can be found
found [here](https://github.schibsted.io/spt-identity/account-sdk-android-web/tree/master/app/src/main/java/com/schibsted/account/example)
.

## Getting started

To implement login with Schibsted account in your app, please first have a look at our
[getting started documentation](https://docs.schibsted.io/schibsted-account/gettingstarted/).
This will help you create a client and configure the necessary data.
[getting started documentation](https://docs.schibsted.io/schibsted-account/gettingstarted/). This
will help you create a client and configure the necessary data.

**Note:** This SDK requires your client to be registered as a `public_mobile_client`. Please email
our [support](mailto:schibstedaccount@schibsted.com) to get help with setting that up. You should
receive both `clientId`, as well as `redirect url` used in steps below.

**Note:** This SDK requires your client to be registered as a `public_mobile_client`. Please
email our [support](mailto:schibstedaccount@schibsted.com) to get help with setting that up.
**Note:** If you have implemented
the [Old Schibsted SDK](https://github.com/schibsted/account-sdk-android) in your app, and want
these users to remain logged in, do not forget to add the SessionStorageConfig to your Client (see
Create a `Client` instance step below).

**Note:** Using [App Links](https://developer.android.com/training/app-links) should be preferred
for [security reasons](https://tools.ietf.org/html/rfc8252#appendix-B.2). To support older Android
Expand All @@ -22,12 +33,14 @@ app via a custom scheme.
### Installation

The SDK is available via [Schibsted Artifactory](https://artifacts.schibsted.io/):

* Using Gradle: `implementation 'com.schibsted.account:account-sdk-android-web:<version>'`

### Usage

1. In your `AndroidManifest.xml`, configure the necessary intent filter for
`RedirectUriReceiverActivity` which handles the auth response after completed user login.
`RedirectUriReceiverActivity` which handles the auth response after completed user
creation/login.
```xml
<activity
android:name="com.schibsted.account.webflows.activities.RedirectUriReceiverActivity">
Expand All @@ -41,23 +54,48 @@ The SDK is available via [Schibsted Artifactory](https://artifacts.schibsted.io/
</intent-filter>
</activity>
```
1. Create a `Client` instance:
As example,
the [Example app](https://github.schibsted.io/spt-identity/account-sdk-android-web/tree/master/app/src/main/java/com/schibsted/account/example)
has the clientId `602525f2b41fa31789a95aa8` and redirect
url `com.sdk-example.pre.602525f2b41fa31789a95aa8://login`, the data section in the manifest will
therefore be:
```xml
<data android:scheme="com.sdk-example.pre.602525f2b41fa31789a95aa8"
android:host="login"/>
```
1. Create a `Client` instance (
see [ExampleApp](https://github.schibsted.io/spt-identity/account-sdk-android-web/blob/master/app/src/main/java/com/schibsted/account/example/ExampleApp.kt)
as reference):
```kotlin
val okHttpClient = OkHttpClient.Builder().build() // this client instance should be shared within your app
val clientConfig = ClientConfiguration(Environment.PRE, "<clientId>", "https://app.example.com/applogin")
val client = Client(applicationContext, clientConfig, okHttpClient)
val okHttpClient = OkHttpClient.Builder().build() // this client instance should be shared within your app
val sessionStorageConfig =
SessionStorageConfig("<oldClientId>", "<oldClientSecret>")
val client = Client(
context = applicationContext,
configuration = clientConfig,
httpClient = okHttpClient,
sessionStorageConfig = sessionStorageConfig
)
```
**Note:** SessionStorageConfig is only needed if your app used
the [Old Schibsted SDK](https://github.com/schibsted/account-sdk-android)
for login and want already logged in users to remain logged in (preferred). If this is not the
scenario, sessionStorageConfig can be ignored.

If you need Retrofit support, wrap the above client instance in a RetrofitClient instance:

If you need Retrofit support, wrap the above client instance in a RetrofitClient instance:
```kotlin
val retrofitClient = RetrofitClient<YourRetrofitInterface>(
client = client,
serviceClass = YourRetrofitInterface::class.java,
retrofitBuilder = Retrofit.Builder().baseUrl("https://your.api.com"),
)
client = client,
serviceClass = YourRetrofitInterface::class.java,
retrofitBuilder = Retrofit.Builder().baseUrl("https://your.api.com"),
)
```

1. Initialise `AuthorizationManagementActivity` on app startup:
1. Initialise `AuthorizationManagementActivity` on app startup (
see [ExampleApp](https://github.schibsted.io/spt-identity/account-sdk-android-web/blob/master/app/src/main/java/com/schibsted/account/example/ExampleApp.kt)
as reference):
```kotlin
class App : Application() {
override fun onCreate() {
Expand All @@ -66,14 +104,16 @@ The SDK is available via [Schibsted Artifactory](https://artifacts.schibsted.io/
val completionIntent = Intent(this, <Activity started after completed login>)
val cancelIntent = Intent(this, <Activity started after cancelled login>)
AuthorizationManagementActivity.setup(
client,
PendingIntent.getActivity(this, 0, completionIntent, 0),
PendingIntent.getActivity(this, 0, cancelIntent, 0)
client = client,
completionIntent = PendingIntent.getActivity(this, 0, completionIntent, 0),
cancelIntent = PendingIntent.getActivity(this, 0, cancelIntent, 0)
)
}
}
```
1. Observe the `AuthResultLiveData` singleton instance to access the logged-in user:
```
1. Observe the `AuthResultLiveData` singleton instance to access the logged-in user (
see [MainActivity](https://github.schibsted.io/spt-identity/account-sdk-android-web/blob/master/app/src/main/java/com/schibsted/account/example/MainActivity.kt)
as reference):
```kotlin
class MainActivity : AppCompatActivity() {
public override fun onCreate(savedInstanceState: Bundle?) {
Expand Down Expand Up @@ -119,14 +159,15 @@ The SDK is available via [Schibsted Artifactory](https://artifacts.schibsted.io/
#### Manual flow

The recommended usage (see above) automatically handles the following cases for you:

* If the user cancels the flow.
* Managing the back stack such that the Custom Tabs instance is cleared from it. See for example
[this article](https://www.rallyhealth.com/back-stack-management-with-chrome-custom-tabs) for
more details.
[this article](https://www.rallyhealth.com/back-stack-management-with-chrome-custom-tabs) for more
details.
* Makes the logged-in user easily accessible via a `LiveData` instance.
But if you want/need more control over the flow you can manually manage the flow.
To do that, follow these steps:

But if you want/need more control over the flow you can manually manage the flow. To do that, follow
these steps:

1. Create a new `Activity` that will receive the auth response via deep link and add it to your
`AndroidManifest.xml`:
Expand Down Expand Up @@ -171,22 +212,25 @@ To do that, follow these steps:
```

## Included functionality

* Single-sign on via web flows, offering one-click login for returning users (via shared cookies in
CustomTabs).
* With support for custom scope values, MFA, etc. See
[here](https://docs.schibsted.io/schibsted-account/guides/authentication/) for more information.
[here](https://docs.schibsted.io/schibsted-account/guides/authentication/) for more
information.
* Automatic and transparent management of user tokens.
* Authenticated requests to backend services can be done via
[`User.makeAuthenticatedRequest`](https://pages.github.schibsted.io/spt-identity/account-sdk-android-web/webflows/com.schibsted.account.webflows.user/-user/make-authenticated-request.html).
[`User.makeAuthenticatedRequest`](https://pages.github.schibsted.io/spt-identity/account-sdk-android-web/webflows/com.schibsted.account.webflows.user/-user/make-authenticated-request.html)
.

If using Retrofit the authenticated request should be done via
[`RetrofitClient.makeAuthenticatedRequest`](https://pages.github.schibsted.io/spt-identity/account-sdk-android-web/webflows/com.schibsted.account.webflows.user/-retrofit-client-facade/make-authenticated-request.html).
[`RetrofitClient.makeAuthenticatedRequest`](https://pages.github.schibsted.io/spt-identity/account-sdk-android-web/webflows/com.schibsted.account.webflows.user/-retrofit-client-facade/make-authenticated-request.html)
.

The SDK will automatically inject the user access token as a Bearer token in the HTTP
Authorization request header.
If the access token is rejected with a `401 Unauthorized` response (e.g. due to having
expired), the SDK will try to use the refresh token to obtain a new access token and then
retry the request once more.
Authorization request header. If the access token is rejected with a `401 Unauthorized`
response (e.g. due to having expired), the SDK will try to use the refresh token to obtain a
new access token and then retry the request once more.

**Note:** If the refresh token request fails, due to the refresh token itself having expired
or been invalidated by the user, the SDK will log the user out.
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,5 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

//Logging
implementation 'com.jakewharton.timber:timber:5.0.1'
implementation "com.jakewharton.timber:timber:$timber_version"
}
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,4 @@
</intent-filter>
</activity>
</application>
</manifest>
</manifest>
19 changes: 13 additions & 6 deletions app/src/main/java/com/schibsted/account/example/ExampleApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,16 @@ class ExampleApp : Application() {
}

private fun initClient() {
val clientConfig =
ClientConfiguration(environment, ClientConfig.clientId, ClientConfig.loginRedirectUri)
client = Client(applicationContext, clientConfig, instance)
val clientConfig = ClientConfiguration(
env = environment,
clientId = ClientConfig.clientId,
redirectUri = ClientConfig.loginRedirectUri
)
client = Client(
context = applicationContext,
configuration = clientConfig,
httpClient = instance
)
}

private fun initAuthorizationManagement() {
Expand All @@ -31,9 +38,9 @@ class ExampleApp : Application() {
cancelIntent.putExtra(LOGIN_FAILED_EXTRA, true)
cancelIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
AuthorizationManagementActivity.setup(
client,
PendingIntent.getActivity(this, 0, completionIntent, 0),
PendingIntent.getActivity(this, 1, cancelIntent, 0)
client = client,
completionIntent = PendingIntent.getActivity(this, 0, completionIntent, 0),
cancelIntent = PendingIntent.getActivity(this, 1, cancelIntent, 0)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ class LoggedInActivity : AppCompatActivity() {
binding.sessionExchangeButton.setOnClickListener {
if (isUserLoggedIn) {
user?.webSessionUrl(
ClientConfig.webClientId,
ClientConfig.webClientRedirectUri,
clientId = ClientConfig.webClientId,
redirectUri = ClientConfig.webClientRedirectUri,
)
{ result: Either<HttpError?, URL> ->
result
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,12 @@ class ManualLoginActivity : AppCompatActivity() {

private fun initResumeButton() {
binding.resumeButton.setOnClickListener {
val user = ExampleApp.client.resumeLastLoggedInUser()
if (user != null) {
startLoggedInActivity(user)
} else {
Toast.makeText(this, "User could not be resumed", Toast.LENGTH_SHORT).show()
ExampleApp.client.resumeLastLoggedInUser { user ->
if (user != null) {
startLoggedInActivity(user)
} else {
Toast.makeText(this, "User could not be resumed", Toast.LENGTH_SHORT).show()
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.5.30'
ext.timber_version = '5.0.1'
repositories {
google()
jcenter()
Expand Down
2 changes: 1 addition & 1 deletion testsupport/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ android {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
}
}
3 changes: 3 additions & 0 deletions webflows/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ dependencies {
api "androidx.browser:browser:1.3.0"
implementation 'com.nimbusds:nimbus-jose-jwt:9.5'

//Logging
implementation "com.jakewharton.timber:timber:$timber_version"

testImplementation 'junit:junit:4.13.2'
testImplementation "io.mockk:mockk:${mockkVersion}"
testImplementation "com.squareup.okhttp3:mockwebserver:${okHttpVersion}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@ class AuthResultLiveData private constructor(private val client: Client) :
}

init {
val resumedUser = client.resumeLastLoggedInUser()
value = if (resumedUser != null) {
Right(resumedUser)
} else {
Left(NotAuthed.NoLoggedInUser)
client.resumeLastLoggedInUser { resumedUser ->
value = if (resumedUser != null) {
Right(resumedUser)
} else {
Left(NotAuthed.NoLoggedInUser)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,9 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import com.schibsted.account.webflows.util.Logging
import com.schibsted.account.webflows.client.Client
import com.schibsted.account.webflows.util.Either.Left
import timber.log.Timber

/**
* Stores state and handles events related to the authorization flow. It functions
Expand Down Expand Up @@ -177,7 +176,7 @@ class AuthorizationManagementActivity : Activity() {
}

private fun handleAuthorizationCanceled() {
Log.d(Logging.SDK_TAG, "Authorization flow canceled by user")
Timber.d("Authorization flow canceled by user")
AuthResultLiveData.get().update(Left(NotAuthed.CancelledByUser))
cancelIntent.send()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.schibsted.account.webflows.api

data class CodeExchangeResponse(val code: String)
Loading

0 comments on commit ba58110

Please sign in to comment.