Skip to content

Commit

Permalink
feat: Create TokenResponse to ensure the AppAuth library doesn't leak
Browse files Browse the repository at this point in the history
- Add README.md
- Rename interface and implementation
Resolves: DCMAW-7374
  • Loading branch information
alex-bradbury committed Dec 19, 2023
1 parent 0cdeb50 commit 09384a8
Show file tree
Hide file tree
Showing 8 changed files with 307 additions and 133 deletions.
135 changes: 135 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# mobile-android-authentication

Implementation of Authentication package

## Installation

To use Authentication in an Android Project:

1. Add the following to the settings.gradle.kts

```kotlin
dependencyResolutionManagement {
...

repositories {

...

maven("https://maven.pkg.github.com/govuk-one-login/mobile-android-authentication") {
if (file("${rootProject.projectDir.path}/github.properties").exists()) {
val propsFile = File("${rootProject.projectDir.path}/github.properties")
val props = Properties().also { it.load(FileInputStream(propsFile)) }
val ghUsername = props["ghUsername"] as String?
val ghToken = props["ghToken"] as String?

credentials {
username = ghUsername
password = ghToken
}
} else {
credentials {
username = System.getenv("USERNAME")
password = System.getenv("TOKEN")
}
}
}
}
}
```

2. For local development, ensure you have a `github.properties` in the project's root which includes your username and an access token
3. Add `implementation("uk.gov.android:authentication:_")` for latest version. Check packages for version information

## Package description

The Authentication package authenticates a users details and enables them to log into their account securely. This is done by providing them with a login session and token.

The package integrates [openID](https://openid.net/developers/how-connect-works/) AppAuth and conforms to its standards, documentation can be found here [AppAuth](https://github.com/openid/AppAuth-Android)

### Types

#### LoginSessionConfiguration

Handles creating the `config` found in `LoginSession`. It requires the following to be initialised:

```kotlin
val authorizeEndpoint: Uri
val clientId: String
val redirectUri: Uri
val scopes: String
val tokenEndpoint: Uri

// Default values
val locale: String = "en"
val prefersEphemeralWebSession: Boolean = true
val responseType: String = ResponseTypeValues.CODE
val vectorsOfTrust: String = "[\"Cl.Cm.P0\"]"
```

#### TokenResponse

Holds the returned token values

```kotlin
val tokenType: String
val accessToken: String
val accessTokenExpirationTime: Long
val idToken: String
val refreshToken: String?
val scope: String
```

#### AppAuthSession

A class to handle the login flow with the given auth provider and conforms to the `LoginSession` protocol.

`present` takes configuration, which comes from `LoginSessionConfiguration`, as a parameter and contains the login information to make the request. It will start an Activity for Result

`finalise` takes the `Intent` received from the Activity started by `present` and provides the `TokenResponse` via a callback

## Example Implementation

### How to use the Authentication package

Don't forget to call `init` with a context before use!

```kotlin
import uk.gov.android.authentication.LoginSession

...

val loginSession: LoginSession = AppAuthSession()
val configuration = LoginSessionConfiguration(
authorizeEndpoint = uri,
clietId = "clientId",
redirectUri = uri,
scopes = "scopes",
tokenEdnpoint = uri
)

loginSession
.init(context)
.present(configuration)


```

Ensure the request code has been registered by the Activity to handle the ActivityResult and call `finalise`

```kotlin
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)

if (requestCode == LoginSession.REQUEST_CODE_AUTH) {
try {
loginSession.finalise(intent) { tokens ->
// Do what you like with the tokens!
// ...
}
} catch (e: Error) {
// handle error
}
}
}
```
4 changes: 3 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ plugins {
alias(libs.plugins.android.kotlin)
alias(libs.plugins.detekt)
alias(libs.plugins.ktlint)
alias(libs.plugins.kotlin.serlialization)
id("maven-publish")
id("authentication.jvm-toolchains")
id("sonarqube-module-config")
Expand Down Expand Up @@ -101,7 +102,8 @@ dependencies {
listOf(
libs.androidx.core.core.ktx,
libs.appauth,
libs.appcompat
libs.appcompat,
libs.kotlinx.serialization.json
).forEach(::implementation)

listOf(
Expand Down
117 changes: 117 additions & 0 deletions app/src/main/java/uk/gov/android/authentication/AppAuthSession.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package uk.gov.android.authentication

import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.core.app.ActivityCompat
import java.util.UUID
import net.openid.appauth.AuthorizationException
import net.openid.appauth.AuthorizationRequest
import net.openid.appauth.AuthorizationResponse
import net.openid.appauth.AuthorizationService
import net.openid.appauth.AuthorizationServiceConfiguration

@Suppress("TooGenericExceptionThrown")
class AppAuthSession : LoginSession {
private var context: Context? = null
private lateinit var authService: AuthorizationService

override fun init(
context: Context
): LoginSession {
if (this.context == null) {
this.context = context
authService = AuthorizationService(context)
}
return this
}

override fun present(
configuration: LoginSessionConfiguration
) {
if (context == null) {
throw Error("Context is null, did you call init?")
}

with(configuration) {
val context = this@AppAuthSession.context!!
val nonce = UUID.randomUUID().toString()

val serviceConfig = AuthorizationServiceConfiguration(
authorizeEndpoint,
tokenEndpoint
)

val builder = AuthorizationRequest.Builder(
serviceConfig,
clientId,
responseType,
redirectUri
).also {
it.apply {
setScopes(scopes)
setUiLocales(locale)
setNonce(nonce)
setAdditionalParameters(
mapOf(
"vtr" to vectorsOfTrust
)
)
}
}

val authRequest = builder.build()

val authIntent = authService.getAuthorizationRequestIntent(authRequest)
ActivityCompat.startActivityForResult(
context as Activity,
authIntent,
REQUEST_CODE_AUTH,
null
)
}
}

override fun finalise(intent: Intent, callback: (tokens: TokenResponse) -> Unit) {
val authorizationResponse = AuthorizationResponse.fromIntent(intent)

if (authorizationResponse == null) {
val exception = AuthorizationException.fromIntent(intent)

throw Exception(exception?.message)
}

val exchangeRequest = authorizationResponse.createTokenExchangeRequest()

authService.performTokenRequest(
exchangeRequest
) { response, exception ->
if (response == null) {
throw Error(exception?.message)
}

callback(createFromAppAuthResponse(response))
}
}

private fun createFromAppAuthResponse(
response: net.openid.appauth.TokenResponse
): TokenResponse {
return TokenResponse(
tokenType = requireNotNull(response.tokenType) { "token type must not be empty" },
accessToken =
requireNotNull(response.accessToken) { "access token must not be empty" },
accessTokenExpirationTime =
requireNotNull(response.accessTokenExpirationTime) {
"Token expiry must not be empty"
},
idToken = requireNotNull(response.idToken) { "id token must not be empty" },
refreshToken = response.refreshToken,
scope = requireNotNull(response.scope) { "scope must not be empty" }
)
}

companion object {
const val REQUEST_CODE_AUTH = 418
}
}
36 changes: 0 additions & 36 deletions app/src/main/java/uk/gov/android/authentication/ILoginSession.kt

This file was deleted.

Loading

0 comments on commit 09384a8

Please sign in to comment.