Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(react-native-test-app-msal): add support for Android #894

Merged
merged 17 commits into from Dec 7, 2021
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/breezy-mugs-shake.md
@@ -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
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
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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, just reusing the id from the tutorials on https://github.com/AzureAD/microsoft-authentication-library-for-android

+ "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
@@ -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:+'
}
@@ -0,0 +1,3 @@
# These properties are required to enable AndroidX for the test app.
android.useAndroidX=true
android.enableJetifier=true
@@ -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>
@@ -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
}
}
@@ -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())
}
@@ -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)
}
}
@@ -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>()
}
}
@@ -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
)
}