Implementation of Authentication package
To use Authentication in an Android Project:
- Add the following to the settings.gradle.kts
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")
}
}
}
}
}- For local development, ensure you have a
github.propertiesin the project's root which includes your username and an access token. Do not commit this file to Version Control - Add
implementation("uk.gov.android:authentication:_")for latest version and check packages for version information
The Authentication package comprises three sub-packages:
- login - 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.
- integrity - checks the app integrity and provides a ClientAttestation and ProofOfPossession that will be used for retrieving an authentication token response.
- jwt - provides ability to verify a JWT with a public key adhering to the JWK specifications.
- localauth - provides functionality for checking the device security level/ type and saving this as a local authentication preference.
The package integrates openID AppAuth and conforms to its standards, documentation can be found here AppAuth.
Handles creating the config found in LoginSession. It requires the following to be initialised:
val authorizeEndpoint: Uri
val clientId: String
val redirectUri: Uri
val scopes: List<Scope>
val tokenEndpoint: Uri
// Default values
val locale: Locale = Locale.EN
val prefersEphemeralWebSession: Boolean = true
val responseType: ResponseType = ResponseType.CODE
val vectorsOfTrust: String = "[\"Cl.Cm.P0\"]"Holds the returned token values:
val tokenType: String
val accessToken: String
val accessTokenExpirationTime: Long
val idToken: String
val refreshToken: String?
val scope: StringCustom error extending Error
val message: String
val type: ErrorTypeA 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
import uk.gov.android.authentication.LoginSession
...
val loginSession: LoginSession = AppAuthSession(context)
val configuration = LoginSessionConfiguration(
authorizeEndpoint = uri,
clietId = "clientId",
redirectUri = uri,
scopes = "scopes",
tokenEdnpoint = uri
)
loginSession.present(configuration)
Ensure the request code has been registered by the Activity to handle the ActivityResult and call finalise
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == LoginSession.REQUEST_CODE_AUTH) {
loginSession.finalise(intent, appIntegrityParameters,
{ tokens: TokenResponse ->
// Do what you like with the tokens!
// ...
},
{t: Throwable ->
// Handle exceptions here
}
)
}
}Creates a JWK that adheres to the backend format requirements:
{
"jwk": {
"kty": "EC",
"use": "sig",
"crv": "P-256",
"x": "18wHLeIgW9wVN6VD1Txgpqy2LszYkMf6J8njVAibvhM",
"y": "-V4dS4UaLMgP_4fY4j8ir7cl1TXlFdAgcx55o7TkcSA"
}
}This will be used to verify the PoP when authenticating.
val jwk = JWK.makeJWK(
x = "<ECPoint_x_inBase64UrlEncoded>",
y = "<ECPoint_y_inBase64UrlEncoded>"
)The AppChecker is an interface that allows for a custom implementation on client side to provide a AppCheckToken. The AppCheckToken is a wrapper that allows for the AppCheckerInterface to be used with different implementations. It contains a JWT provided by a backend service.
Implementation - this is a Firebase specific implementation
class AppCheckImpl @Inject constructor(
appCheckFactory: AppCheckProviderFactory,
context: Context
) : AppChecker {
private val appCheck = Firebase.appCheck
init {
Firebase.appCheck.installAppCheckProviderFactory(
appCheckFactory
)
Firebase.initialize(context)
}
override suspend fun getAppCheckToken(): Result<AppCheckToken> {
return try {
Result.success(
AppCheckToken(appCheck.limitedUseAppCheckToken.await().token)
)
} catch (e: FirebaseException) {
Result.failure(e)
}
}
}The AttestationCaller is an interface that allows for custom implementation on checking the token provided from the AppChecker and returning an AttestationResponse. The AttestationResponse provides an attestation in JWT format and an expiry time. These will be use to confirm if a new AppCheck is required when logging in and in the process of obtaining access tokens.
class AttestationCallerImpl @Inject constructor(
@ApplicationContext
private val context: Context,
private val httpClient: GenericHttpClient
) : AttestationCaller {
override suspend fun call(
token: String,
jwk: JWK.JsonWebKey
): AttestationResponse {
// The token and any additional parameters can be amended accordingly and provided where needed, this is just an example of a possible implementation
val request = ApiRequest.Post(
url = "https://attestation-url.co.uk/endpoint",
body = jwk,
headers = listOf(
"appCheckToken" to token,
AttestationCaller.CONTENT_TYPE to AttestationCaller.CONTENT_TYPE_VALUE
)
)
return when (val apiResponse = httpClient.makeRequest(request)) {
is ApiResponse.Success<*> -> {
// Handle successful attestation response
}
is ApiResponse.Failure -> AttestationResponse.Failure(
apiResponse.error.message ?: NETWORK_ERROR,
apiResponse.error
)
// e.g. Offline
else -> AttestationResponse.Failure(NETWORK_ERROR)
}
}
companion object {
const val NETWORK_ERROR = "Network error"
}
}The ProofOfPossessionGenerator object creates a Proof of Possession (PoP) that will be used in the authentication call, as part of a header. It adheres to the following scheme and it is a signed JWT contained within the SignedPoP. This will be used and verified by the backend to ensure the app is genuine. It adheres to the following requirements:
Header
{
"alg": "ES256"
}Body
{
"iss": "<OAuth client ID",
"aud": "https://token.account.gov.uk",
"exp": 1234567890,
"jti": "d3e8e382-4691-4b1f-83c1-4454f75bd930"
}Implementation
val pop = ProofOfPossessionGenerator.createBase64PoP(iss, aud)The AppIntegrityManager combines the structures explained above and creates a provides the functionality of these into a service that retrieves a ClientAttestation and creates a Proof of Possession. The AppIntegrityConfiguration provides the AttestationCaller, AppChecker, and KeyStoreManager specific implementation to be provided to the AppIntegrityManager.
An example of the AppIntegrityManager and a possible implementation is available in the FirebaseAppIntegrityManager
The LocalAuthManager allows a consumer to check the device security level and what is available and enabled (for example biometrics, passcode, etc). Based on this, it will save preferences accordingly.
It requires a DeviceBiometricsManager and a LocalAuthPreferenceRepo which allows for the device checks and storing/ updating the reference accordingly.
val biometricManager = BiometricManager.from(context)
val kgm = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
DeviceBiometricsManagerImpl(biometricManager, kgm)Example of LocalAuthPreferenceRepo implementation
class LocalAuthPreferenceRepositoryImpl(private val context: Context) : LocalAuthPreferenceRepo {
private val sharedPrefs = context.getSharedPreferences(
SHARED_PREFS_ID,
Context.MODE_PRIVATE
)
override fun setLocalAuthPref(pref: LocalAuthPreference) {
with(sharedPrefs.edit()) {
putString(BIO_PREF, pref.toString())
apply()
}
}
override fun getLocalAuthPref(): LocalAuthPreference? {
return when (sharedPrefs.getString(BIO_PREF, null)) {
LocalAuthPreference.Enabled(true).toString() -> LocalAuthPreference.Enabled(true)
LocalAuthPreference.Enabled(false).toString() -> LocalAuthPreference.Enabled(false)
LocalAuthPreference.Disabled.toString() -> LocalAuthPreference.Disabled
else -> null
}
}
Gradle secure hash algorithm (SHA) pinning is in place through the distributionSha256Sum attribute in gradle-wrapper.properties. This means the gradle-wrapper must upgrade through the ./gradlew wrapper command.
Example gradle-wrapper.properties
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=2db75c40782f5e8ba1fc278a5574bab070adccb2d21ca5a6e5ed840888448046
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
Use the following command to update the gradle wrapper. Run the same command twice, reason.
./gradlew wrapper --gradle-version=8.10.2 --distribution-type=bin --gradle-distribution-sha256-sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26Flags:
gradle-versionself explanatorydistribution-typeset tobinshort for binary refers to the gradle bin, often in this formatgradle-8.10.2-bin.zipgradle-distribution-sha256-sumthe SHA 256 checksum from this page, pick the binary checksum for the version used
The gradle wrapper update can include:
- gradle-wrapper.jar
- gradle-wrapper.properties
- gradlew
- gradlew.bat
You can use the following command to check the SHA 256 checksum of a file
shasum -a 256 gradle-8.10.2-bin.zipThere are GitHub Actions workflows for a hotfix pull request and merging a hotfix to a temporary hotfix branch.
The temporary hotfix branch is currently expected to be named "temp/hotfix". If a different name is
desired please edit the value under "branches:" in .github/workflows/on_push_hotfix.yml.
The hotfix branch name should be in the format "hotfix/M.m.p".
Once the hotfix PR has been approved and the "Squash and merge" button pressed, the merge title must be in the format "Merge pull request #xxx from govuk-one-login/release/M.m.p" to allow for the correct version to be extracted and used as a tag.