Skip to content

Commit

Permalink
feat(react-native-test-app-msal): add support for Android (#894)
Browse files Browse the repository at this point in the history
  • Loading branch information
tido64 committed Dec 7, 2021
1 parent 99b76a3 commit 1c5e36d
Show file tree
Hide file tree
Showing 33 changed files with 1,107 additions and 39 deletions.
5 changes: 5 additions & 0 deletions .changeset/breezy-mugs-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rnx-kit/react-native-test-app-msal": minor
---

Added support for Android
25 changes: 25 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,31 @@ jobs:
yarn bundle+esbuild
shell: bash
working-directory: packages/test-app
build-android:
name: "Build Android"
runs-on: ubuntu-latest
steps:
- name: Set up Node 14
uses: actions/setup-node@v2.4.1
with:
node-version: 14
- name: Checkout
uses: actions/checkout@v2.4.0
with:
fetch-depth: 0
- name: Cache /.yarn-offline-mirror
uses: actions/cache@v2.1.7
with:
path: .yarn-offline-mirror
key: ${{ runner.os }}-${{ hashFiles('yarn.lock') }}-2
- name: Install npm dependencies
run: yarn ci
env:
CI_SKIP_GO: 1
- name: Build Android app
run: |
./gradlew clean build
working-directory: packages/test-app/android
build-ios:
name: "Build iOS"
runs-on: macos-11
Expand Down
25 changes: 21 additions & 4 deletions packages/react-native-test-app-msal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ Add an entry for the account switcher in your `app.json`, e.g.:
"appKey": "MyTestApp",
+ },
+ {
+ "appKey": "MicrosoftAccounts"
+ "appKey": "com.microsoft.reacttestapp.msal.MicrosoftAccountsActivity",
+ "displayName": "MicrosoftAccounts (Android)"
+ },
+ {
+ "appKey": "MicrosoftAccounts",
+ "displayName": "MicrosoftAccounts (iOS/macOS)"
}
],
"resources": {
Expand All @@ -77,16 +82,28 @@ then fill out the following fields in `app.json`:
"appKey": "MyTestApp",
},
{
"appKey": "MicrosoftAccounts"
"appKey": "com.microsoft.reacttestapp.msal.MicrosoftAccountsActivity",
"displayName": "MicrosoftAccounts (Android)"
},
{
"appKey": "MicrosoftAccounts",
"displayName": "MicrosoftAccounts (iOS/macOS)"
}
],
+ "android": {
+ "package": "com.contoso.MyTestApp"
+ },
+ "ios": {
+ "bundleIdentifier": "com.contoso.MyTestApp"
+ },
+ "macos": {
+ "bundleIdentifier": "com.contoso.MyTestApp"
+ },
+ "react-native-test-app-msal": {
+ "clientId": "00000000-0000-0000-0000-000000000000",
+ "clientId": "4b0db8c2-9f26-4417-8bde-3f0e3656f8e0",
+ "msaScopes": ["user.read"],
+ "orgScopes": ["<Application ID URL>/scope"]
+ "orgScopes": ["user.read"],
+ "signatureHash": "1wIqXSqBj7w+h11ZifsnqwgyKrY="
+ },
"resources": {
"android": ["dist/res", "dist/main.android.jsbundle"],
Expand Down
162 changes: 162 additions & 0 deletions packages/react-native-test-app-msal/android/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import groovy.json.JsonOutput
import groovy.json.JsonSlurper

import java.nio.file.Paths

buildscript {
ext.ensureProperty = { config, property ->
if (!config.containsKey(property)) {
throw new MissingPropertyException("Missing '$property' in 'react-native-test-app-msal' config")
}
return config[property]
}

ext.findFile = { fileName ->
def currentDirPath = rootDir == null ? null : rootDir.toString()

while (currentDirPath != null) {
def currentDir = file(currentDirPath);
def requestedFile = Paths.get(currentDirPath, fileName).toFile()

if (requestedFile.exists()) {
return requestedFile
}

currentDirPath = currentDir.getParent()
}

return null
}

ext.findNodeModulesPath = { packageName ->
return findFile(Paths.get('node_modules', packageName).toString())
}

ext.getExtProp = { prop, defaultValue ->
return rootProject.ext.has(prop) ? rootProject.ext.get(prop) : defaultValue
}

ext.getStringArray = { config, property ->
return (!config.containsKey(property) || config[property].size() == 0)
? ""
: "\"${config[property].join('", "')}\""
}

ext.kotlinVersion = getExtProp('kotlinVersion', '1.5.31')

repositories {
google()
mavenCentral()
}

dependencies {
classpath "com.android.tools.build:gradle:${getExtProp('androidPluginVersion', '4.2.2')}"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
}

plugins {
id "com.android.library"
id "kotlin-android"
}

repositories {
maven {
url("${findNodeModulesPath('react-native')}/android")
}

google()
mavenCentral()

// https://github.com/AzureAD/microsoft-authentication-library-for-android#step-1-declare-dependency-on-msal
maven {
url 'https://pkgs.dev.azure.com/MicrosoftDeviceSDK/DuoSDK-Public/_packaging/Duo-SDK-Feed/maven/v1'
}
}

android {
def manifest = new JsonSlurper().parseText(findFile('app.json').text)
def config = manifest["react-native-test-app-msal"]
if (config == null) {
throw new MissingPropertyException("Missing 'react-native-test-app-msal' field in 'app.json'")
}

def signatureHash = ensureProperty(config, "signatureHash")

compileSdkVersion getExtProp('compileSdkVersion', 31)
defaultConfig {
minSdkVersion getExtProp('minSdkVersion', 23)
targetSdkVersion getExtProp('targetSdkVersion', 29)

buildConfigField "String[]",
"ReactTestAppMSAL_msaScopes",
"new String[]{${getStringArray(config, "msaScopes")}}"
buildConfigField "String[]",
"ReactTestAppMSAL_orgScopes",
"new String[]{${getStringArray(config, "orgScopes")}}"

manifestPlaceholders = [
msalRedirectUriPath: "/$signatureHash"
]
}
sourceSets {
def clientId = ensureProperty(config, "clientId")

def appProject = rootProject.subprojects.find { it.name == "app" }
def applicationId = appProject.android.defaultConfig.applicationId
def redirectUri = "msauth://$applicationId/${URLEncoder.encode(signatureHash, "UTF-8")}"

def generatedResDir = file("$buildDir/generated/react-native-test-app-msal/src/main/res/")
generatedResDir.mkdirs()

task copyMsalConfig(type: Copy) {
def generatedRawDir = file("$generatedResDir/raw")
generatedRawDir.mkdirs()

def msalConfig = file("$temporaryDir/msal_config.json")
msalConfig.withWriter {
it << JsonOutput.toJson([
authorities: [
[
type: "AAD",
audience: [
type: "AzureADandPersonalMicrosoftAccount"
],
default: true
],
[
type: "AAD",
audience: [
type: "PersonalMicrosoftAccount"
],
],
],
client_id: clientId,
redirect_uri: redirectUri,
broker_redirect_uri_registered: false,
account_mode: "MULTIPLE"
])
}

from msalConfig
into generatedRawDir
}

preBuild.dependsOn(copyMsalConfig)

main.res.srcDirs += generatedResDir
}
}

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"

implementation 'androidx.activity:activity-ktx:1.4.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'com.microsoft.identity.client:msal:2.2.1'

//noinspection GradleDynamicVersion
implementation 'com.facebook.react:react-native:+'
}
3 changes: 3 additions & 0 deletions packages/react-native-test-app-msal/android/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# These properties are required to enable AndroidX for the test app.
android.useAndroidX=true
android.enableJetifier=true
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.microsoft.reacttestapp.msal">

<application>
<activity android:name=".MicrosoftAccountsActivity" />
<activity
android:name="com.microsoft.identity.client.BrowserTabActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="${applicationId}"
android:path="${msalRedirectUriPath}"
android:scheme="msauth" />
</intent-filter>
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.microsoft.reacttestapp.msal

import com.microsoft.identity.client.IAccount

data class Account(
val userPrincipalName: String,
val accountType: AccountType
) {
override fun toString(): String {
return "$userPrincipalName (${accountType.description()})"
}
}

fun List<IAccount>.find(userPrincipalName: String, accountType: AccountType): IAccount? {
return find {
it.username == userPrincipalName && it.accountType() == accountType
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.microsoft.reacttestapp.msal

import com.microsoft.identity.client.IAccount

enum class AccountType(val type: String) {
MICROSOFT_ACCOUNT("MicrosoftAccount"),
ORGANIZATIONAL("Organizational");

companion object {
// Source: https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens
private const val MSA_TENANT = "9188040d-6c67-4c5b-b112-36a304b66dad"

fun fromIssuer(issuer: String): AccountType {
return if (issuer.contains(MSA_TENANT))
MICROSOFT_ACCOUNT
else
ORGANIZATIONAL
}
}

fun description(): String {
return when (this) {
MICROSOFT_ACCOUNT -> "personal"
ORGANIZATIONAL -> "work"
}
}
}

fun IAccount.accountType(): AccountType {
return AccountType.fromIssuer(claims?.get("iss").toString())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.microsoft.reacttestapp.msal

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView

class AccountsAdapter(
context: Context,
private val accounts: MutableList<Account>
) : ArrayAdapter<Account>(context, R.layout.account_item, accounts) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val layoutInflater = LayoutInflater.from(parent.context)
val view = convertView ?: layoutInflater.inflate(R.layout.account_item, parent, false)

val (userPrincipalName, accountType) = accounts[position]
view.findViewById<TextView>(R.id.username).text = userPrincipalName

val accountTypeText = parent.context.resources.getString(R.string.account_type)
view.findViewById<TextView>(R.id.account_type).text =
String.format(accountTypeText, accountType.description())

return view
}

override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
return getView(position, convertView, parent)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.microsoft.reacttestapp.msal

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class AccountsViewModel : ViewModel() {
val accounts: MutableLiveData<List<Account>> by lazy {
MutableLiveData<List<Account>>()
}

val canAddAccount: MutableLiveData<Boolean> by lazy {
MutableLiveData<Boolean>(true)
}

val canSignOut: MutableLiveData<Boolean> by lazy {
MutableLiveData<Boolean>(false)
}

val selectedAccount: MutableLiveData<Account> by lazy {
MutableLiveData<Account>()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.microsoft.reacttestapp.msal

import com.microsoft.identity.common.exception.BaseException

data class AuthError(
val type: AuthErrorType,
val correlationId: String,
val message: String?
) {
constructor(exception: BaseException) : this(
AuthErrorType.fromMsalException(exception),
exception.correlationId ?: TokenBroker.EMPTY_GUID,
exception.message
)
}

0 comments on commit 1c5e36d

Please sign in to comment.