зеркало из https://github.com/microsoft/rnx-kit.git
feat(react-native-test-app-msal): MSAL module for react-native-test-app (#730)
This commit is contained in:
Родитель
3bbcd777b4
Коммит
ef7b6705d3
|
@ -135,6 +135,7 @@ individually, as features are added and fixes are made.
|
|||
| [@rnx-kit/metro-serializer-esbuild](https://github.com/microsoft/rnx-kit/tree/main/packages/metro-serializer-esbuild) | Experimental esbuild serializer for Metro |
|
||||
| [@rnx-kit/metro-service](https://github.com/microsoft/rnx-kit/tree/main/packages/metro-service) | Metro service for bundling and bundle-serving |
|
||||
| [@rnx-kit/metro-swc-worker](https://github.com/microsoft/rnx-kit/tree/main/packages/metro-swc-worker) | Metro transform worker that uses swc under the hood |
|
||||
| [@rnx-kit/react-native-test-app-msal](https://github.com/microsoft/rnx-kit/tree/main/packages/react-native-test-app-msal) | Microsoft Authentication Library (MSAL) module for react-native-test-app |
|
||||
| [@rnx-kit/third-party-notices](https://github.com/microsoft/rnx-kit/tree/main/packages/third-party-notices) | Library and tool to build a third party notices file based on a js bundle's source map |
|
||||
| [@rnx-kit/tools-language](https://github.com/microsoft/rnx-kit/tree/main/packages/tools-language) | A collection of supplemental JavaScript functions and types |
|
||||
| [@rnx-kit/tools-node](https://github.com/microsoft/rnx-kit/tree/main/packages/tools-node) | A collection of supplemental NodeJS functions and types |
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "minor",
|
||||
"comment": "MSAL module for react-native-test-app",
|
||||
"packageName": "@rnx-kit/react-native-test-app-msal",
|
||||
"email": "4123478+tido64@users.noreply.github.com",
|
||||
"dependentChangeType": "patch"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
disabled_rules:
|
||||
- function_body_length
|
||||
- opening_brace # Conflicts with SwiftFormat
|
||||
- trailing_comma
|
|
@ -0,0 +1,98 @@
|
|||
# @rnx-kit/react-native-test-app-msal
|
||||
|
||||
[Microsoft Authentication Library](http://aka.ms/aadv2) (MSAL) module for
|
||||
[React Native Test App](https://github.com/microsoft/react-native-test-app#readme).
|
||||
|
||||
## Install
|
||||
|
||||
Add `@rnx-kit/react-native-test-app-msal` as a dev dependency:
|
||||
|
||||
```
|
||||
yarn add @rnx-kit/react-native-test-app-msal --dev
|
||||
```
|
||||
|
||||
or if you're using `npm`:
|
||||
|
||||
```
|
||||
npm add --save-dev @rnx-kit/react-native-test-app-msal
|
||||
```
|
||||
|
||||
### iOS/macOS
|
||||
|
||||
We need to set the deployment target for iOS and macOS to 14.0 and 11.0
|
||||
respectively, and add `MSAL` to `Podfile`:
|
||||
|
||||
```diff
|
||||
+platform :ios, '14.0' # If targeting iOS, discard the line below
|
||||
+platform :macos, '11.0' # If targeting macOS, discard the line above
|
||||
+
|
||||
require_relative '../node_modules/react-native-test-app/test_app'
|
||||
|
||||
workspace 'MyTestApp.xcworkspace'
|
||||
|
||||
+use_test_app! do |target|
|
||||
+ target.app do
|
||||
+ # We must use modular headers here otherwise Swift compiler will fail
|
||||
+ pod 'MSAL', :modular_headers => true
|
||||
+ end
|
||||
+end
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Add an entry for the account switcher in your `app.json`, e.g.:
|
||||
|
||||
```diff
|
||||
{
|
||||
"name": "MyTestApp",
|
||||
"displayName": "MyTestApp",
|
||||
"components": [
|
||||
{
|
||||
"appKey": "MyTestApp",
|
||||
+ },
|
||||
+ {
|
||||
+ "appKey": "MicrosoftAccounts"
|
||||
}
|
||||
],
|
||||
"resources": {
|
||||
"android": ["dist/res", "dist/main.android.bundle"],
|
||||
"ios": ["dist/assets", "dist/main.ios.jsbundle"],
|
||||
"macos": ["dist/assets", "dist/main.macos.jsbundle"],
|
||||
"windows": ["dist/assets", "dist/main.windows.bundle"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Register your app with a unique bundle identifier to get your Azure Active
|
||||
Directory client identifier and related scopes
|
||||
([quickstart here](https://docs.microsoft.com/en-gb/azure/active-directory/develop/quickstart-v2-ios#register-and-download-your-quickstart-app)),
|
||||
then fill out the following fields in `app.json`:
|
||||
|
||||
```diff
|
||||
{
|
||||
"name": "MyTestApp",
|
||||
"displayName": "MyTestApp",
|
||||
"components": [
|
||||
{
|
||||
"appKey": "MyTestApp",
|
||||
},
|
||||
{
|
||||
"appKey": "MicrosoftAccounts"
|
||||
}
|
||||
],
|
||||
+ "ios": {
|
||||
+ "bundleIdentifier": "com.contoso.MyTestApp"
|
||||
+ },
|
||||
+ "react-native-test-app-msal": {
|
||||
+ "clientId": "00000000-0000-0000-0000-000000000000",
|
||||
+ "msaScopes": ["user.read"],
|
||||
+ "orgScopes": ["<Application ID URL>/scope"]
|
||||
+ },
|
||||
"resources": {
|
||||
"android": ["dist/res", "dist/main.android.jsbundle"],
|
||||
"ios": ["dist/assets", "dist/main.ios.jsbundle"],
|
||||
"macos": ["dist/assets", "dist/main.macos.jsbundle"],
|
||||
"windows": ["dist/assets", "dist/main.windows.bundle"]
|
||||
}
|
||||
}
|
||||
```
|
|
@ -0,0 +1,23 @@
|
|||
require 'json'
|
||||
|
||||
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
||||
version = package['version']
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'ReactTestApp-MSAL'
|
||||
s.version = version
|
||||
s.author = { package['author']['name'] => package['author']['email'] }
|
||||
s.license = package['license']
|
||||
s.homepage = package['homepage']
|
||||
s.source = { :git => package['repository']['url'], :tag => "#{package['name']}_v#{version}" }
|
||||
s.summary = package['description']
|
||||
|
||||
s.ios.deployment_target = '14.0'
|
||||
s.osx.deployment_target = '11.0'
|
||||
|
||||
s.dependency 'MSAL'
|
||||
|
||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
|
||||
|
||||
s.source_files = 'ios/*.swift'
|
||||
end
|
|
@ -0,0 +1,71 @@
|
|||
@objc
|
||||
public enum AccountType: Int, CaseIterable {
|
||||
case microsoftAccount
|
||||
case organizational
|
||||
}
|
||||
|
||||
@objc
|
||||
public final class Account: NSObject, Identifiable {
|
||||
// swiftlint:disable:next identifier_name
|
||||
public var id: String {
|
||||
"\(accountType.description):\(userPrincipalName)"
|
||||
}
|
||||
|
||||
let userPrincipalName: String
|
||||
let accountType: AccountType
|
||||
|
||||
init(userPrincipalName: String, accountType: AccountType) {
|
||||
self.userPrincipalName = userPrincipalName
|
||||
self.accountType = accountType
|
||||
}
|
||||
}
|
||||
|
||||
extension AccountType {
|
||||
static func from(issuer: String) -> AccountType {
|
||||
issuer.contains(TokenBroker.Constants.MicrosoftAccountTenant)
|
||||
? .microsoftAccount
|
||||
: .organizational
|
||||
}
|
||||
|
||||
static func from(string: String) -> AccountType {
|
||||
allCases.first { $0.description == string } ?? .organizational
|
||||
}
|
||||
|
||||
var authority: URL! {
|
||||
switch self {
|
||||
case .microsoftAccount:
|
||||
return URL(string: "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize")
|
||||
case .organizational:
|
||||
return URL(string: "https://login.microsoftonline.com/common/")
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .microsoftAccount:
|
||||
return "personal"
|
||||
case .organizational:
|
||||
return "work"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element: Account {
|
||||
func find(userPrincipalName: String, accountType: AccountType) -> Account? {
|
||||
first {
|
||||
$0.accountType == accountType && $0.userPrincipalName == userPrincipalName
|
||||
}
|
||||
}
|
||||
|
||||
func find(string: String) -> Account? {
|
||||
let components = string.split(
|
||||
separator: ":",
|
||||
maxSplits: 1,
|
||||
omittingEmptySubsequences: false
|
||||
)
|
||||
return find(
|
||||
userPrincipalName: String(components[1]),
|
||||
accountType: AccountType.from(string: String(components[0]))
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
// "Forward-declare" RCTBridge to avoid dependency on React-Core
|
||||
typealias RCTBridge = AnyObject
|
||||
|
||||
#if os(iOS)
|
||||
public typealias RTAViewController = UIViewController
|
||||
typealias RTAHostingController = UIHostingController
|
||||
#else
|
||||
public typealias RTAViewController = NSViewController
|
||||
typealias RTAHostingController = NSHostingController
|
||||
#endif
|
||||
|
||||
final class ObservableHostingController: ObservableObject {
|
||||
// swiftlint:disable:next identifier_name
|
||||
@Published var id: Int = 0
|
||||
|
||||
let hostingController: RTAViewController
|
||||
|
||||
init(_ hostingController: RTAViewController) {
|
||||
self.hostingController = hostingController
|
||||
}
|
||||
}
|
||||
|
||||
@objc(MicrosoftAccounts)
|
||||
public final class AccountsHostingController: RTAViewController {
|
||||
@objc
|
||||
init(bridge _: RCTBridge) {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
dynamic required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
let contentView = NSStackView()
|
||||
|
||||
override public func loadView() {
|
||||
contentView.orientation = .vertical
|
||||
contentView.edgeInsets = NSEdgeInsets(top: 24, left: 24, bottom: 24, right: 24)
|
||||
view = contentView
|
||||
}
|
||||
#endif
|
||||
|
||||
override public func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
let rootView = AccountsView().environmentObject(ObservableHostingController(self))
|
||||
let hostingController = RTAHostingController(rootView: rootView)
|
||||
|
||||
addChild(hostingController)
|
||||
|
||||
#if os(iOS)
|
||||
hostingController.view.autoresizingMask = view.autoresizingMask
|
||||
hostingController.view.frame = view.frame
|
||||
view.addSubview(hostingController.view)
|
||||
hostingController.didMove(toParent: self)
|
||||
#else
|
||||
contentView.addView(hostingController.view, in: .leading)
|
||||
#endif
|
||||
}
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
import SwiftUI
|
||||
|
||||
struct AccountsView: View {
|
||||
private lazy var config = Config.load()
|
||||
|
||||
@EnvironmentObject private var hostingController: ObservableHostingController
|
||||
|
||||
@State private var accounts: [Account]
|
||||
@State private var didLoad = false
|
||||
@State private var formDisabled = false
|
||||
@State private var selectAccountType = false
|
||||
|
||||
@State private var selectedAccount: Account? {
|
||||
didSet {
|
||||
onAccountChanged(selectedAccount)
|
||||
}
|
||||
}
|
||||
|
||||
init(accounts: [Account] = TokenBroker.shared.allAccounts()) {
|
||||
self.accounts = accounts
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
Picker("Account:", selection: $selectedAccount) {
|
||||
ForEach(accounts) { account in
|
||||
#if os(iOS)
|
||||
VStack(alignment: .leading) {
|
||||
Text(account.userPrincipalName)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
Text("Account type: \(account.accountType.description)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.secondary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
}
|
||||
.tag(account as Account?)
|
||||
#else
|
||||
Text("\(account.userPrincipalName) (\(account.accountType.description))")
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
.tag(account as Account?)
|
||||
#endif
|
||||
}
|
||||
.truncationMode(.middle)
|
||||
}
|
||||
.onChange(of: selectedAccount) {
|
||||
// `didSet` is not called when selection is changed
|
||||
onAccountChanged($0)
|
||||
}
|
||||
.disabled(accounts.isEmpty)
|
||||
#if os(iOS)
|
||||
Button("Add Account…") { selectAccountType = true }
|
||||
.actionSheet(isPresented: $selectAccountType) {
|
||||
ActionSheet(title: Text("Select account type"), buttons: [
|
||||
.default(Text("Personal")) {
|
||||
var mutableSelf = self
|
||||
mutableSelf.onAddAccount(accountType: .microsoftAccount)
|
||||
},
|
||||
.default(Text("Work or School")) {
|
||||
var mutableSelf = self
|
||||
mutableSelf.onAddAccount(accountType: .organizational)
|
||||
},
|
||||
.cancel(),
|
||||
])
|
||||
}
|
||||
#else
|
||||
Button("Add Personal Account…") {
|
||||
var mutableSelf = self
|
||||
mutableSelf.onAddAccount(accountType: .microsoftAccount)
|
||||
}
|
||||
Button("Add Work or School Account…") {
|
||||
var mutableSelf = self
|
||||
mutableSelf.onAddAccount(accountType: .organizational)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
if selectedAccount != nil {
|
||||
Section {
|
||||
#if os(iOS)
|
||||
if #available(iOS 15.0, macOS 12.0, *) {
|
||||
Button("Sign Out", role: .destructive) { onSignOut() }
|
||||
} else {
|
||||
Button("Sign Out") { onSignOut() }
|
||||
.foregroundColor(Color.red)
|
||||
}
|
||||
#else
|
||||
Button("Sign Out") { onSignOut() }
|
||||
.foregroundColor(Color.red)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
if accounts.count > 1 {
|
||||
Section {
|
||||
#if os(iOS)
|
||||
if #available(iOS 15.0, macOS 12.0, *) {
|
||||
Button("Remove All Accounts", role: .destructive) {
|
||||
onRemoveAllAccounts()
|
||||
}
|
||||
} else {
|
||||
Button("Remove All Accounts") { onRemoveAllAccounts() }
|
||||
.foregroundColor(Color.red)
|
||||
}
|
||||
#else
|
||||
Button("Remove All Accounts") { onRemoveAllAccounts() }
|
||||
.foregroundColor(Color.red)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
guard !didLoad else {
|
||||
return
|
||||
}
|
||||
|
||||
defer {
|
||||
didLoad = true
|
||||
}
|
||||
|
||||
guard let secret = SecretStore.get() else {
|
||||
return
|
||||
}
|
||||
|
||||
selectedAccount = accounts.find(string: secret)
|
||||
}
|
||||
.disabled(formDisabled)
|
||||
}
|
||||
|
||||
private func onAccountChanged(_ account: Account?) {
|
||||
guard didLoad else {
|
||||
return
|
||||
}
|
||||
|
||||
TokenBroker.shared.currentAccount = account
|
||||
|
||||
guard let accountId = account?.id else {
|
||||
SecretStore.remove()
|
||||
return
|
||||
}
|
||||
|
||||
SecretStore.set(secret: accountId)
|
||||
}
|
||||
|
||||
private mutating func onAddAccount(accountType: AccountType) {
|
||||
formDisabled = true
|
||||
|
||||
let mutableSelf = self
|
||||
TokenBroker.shared.acquireToken(
|
||||
scopes: config.scopes(for: accountType),
|
||||
userPrincipalName: nil,
|
||||
accountType: accountType,
|
||||
sender: hostingController.hostingController
|
||||
) { userPrincipalName, _, _ in
|
||||
let allAccounts = TokenBroker.shared.allAccounts()
|
||||
mutableSelf.accounts = allAccounts
|
||||
mutableSelf.selectedAccount = allAccounts.find(
|
||||
userPrincipalName: userPrincipalName,
|
||||
accountType: accountType
|
||||
)
|
||||
mutableSelf.formDisabled = false
|
||||
}
|
||||
}
|
||||
|
||||
private func onRemoveAllAccounts() {
|
||||
formDisabled = true
|
||||
|
||||
TokenBroker.shared.removeAllAccounts(sender: hostingController.hostingController)
|
||||
|
||||
accounts = []
|
||||
selectedAccount = nil
|
||||
formDisabled = false
|
||||
}
|
||||
|
||||
private func onSignOut() {
|
||||
formDisabled = true
|
||||
|
||||
TokenBroker.shared.signOut(sender: hostingController.hostingController) { _, _ in
|
||||
accounts = TokenBroker.shared.allAccounts()
|
||||
selectedAccount = nil
|
||||
formDisabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AccountsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AccountsView(accounts: [
|
||||
Account(userPrincipalName: "arnold@contoso.com", accountType: .organizational),
|
||||
Account(userPrincipalName: "arnold@contoso.com", accountType: .microsoftAccount),
|
||||
])
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import Foundation
|
||||
|
||||
struct Config: Decodable {
|
||||
let clientId: String
|
||||
let msaScopes: [String]?
|
||||
let orgScopes: [String]?
|
||||
|
||||
static func load() -> Config {
|
||||
guard let manifestURL = Bundle.main.url(forResource: "app", withExtension: "json"),
|
||||
let data = try? Data(contentsOf: manifestURL, options: .uncached)
|
||||
else {
|
||||
fatalError("Failed to load 'app.json'")
|
||||
}
|
||||
|
||||
guard let manifest = try? JSONDecoder().decode(Manifest.self, from: data) else {
|
||||
fatalError("Failed to parse 'app.json'")
|
||||
}
|
||||
|
||||
return manifest.msalConfig
|
||||
}
|
||||
|
||||
public func scopes(for accountType: AccountType) -> [String] {
|
||||
switch accountType {
|
||||
case .microsoftAccount:
|
||||
return msaScopes ?? []
|
||||
case .organizational:
|
||||
return orgScopes ?? []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Manifest: Decodable {
|
||||
let msalConfig: Config
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case msalConfig = "react-native-test-app-msal"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
/* Dummy file so @react-native-community/cli recognizes this as an iOS package */
|
|
@ -0,0 +1,58 @@
|
|||
import Foundation
|
||||
import Security
|
||||
|
||||
struct SecretStore {
|
||||
static func get() -> String? {
|
||||
var result: AnyObject?
|
||||
SecItemCopyMatching(CFDictionary.query(for: nil, returnData: true), &result)
|
||||
guard let data = result as? Data else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
static func remove() {
|
||||
SecItemDelete(CFDictionary.query())
|
||||
}
|
||||
|
||||
static func set(secret: String) {
|
||||
let data = secret.data(using: .utf8)
|
||||
let status = SecItemAdd(CFDictionary.query(for: data), nil)
|
||||
if status != 0 {
|
||||
if status == errSecDuplicateItem {
|
||||
let attributesToUpdate = [kSecValueData: data as Any] as CFDictionary
|
||||
SecItemUpdate(CFDictionary.query(), attributesToUpdate)
|
||||
} else {
|
||||
if let message = SecCopyErrorMessageString(status, nil) as String? {
|
||||
NSLog("Failed to set secret: \(message)")
|
||||
} else {
|
||||
NSLog("Failed to set secret (code \(status))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CFDictionary {
|
||||
static func query(for secret: Data? = nil, returnData: Bool = false) -> CFDictionary {
|
||||
let service = "com.microsoft.ReactTestApp-MSAL"
|
||||
let account = "account"
|
||||
|
||||
guard let secret = secret else {
|
||||
return [
|
||||
kSecAttrService: service,
|
||||
kSecAttrAccount: account,
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecReturnData: returnData,
|
||||
] as CFDictionary
|
||||
}
|
||||
|
||||
return [
|
||||
kSecAttrService: service,
|
||||
kSecAttrAccount: account,
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecValueData: secret as Any,
|
||||
] as CFDictionary
|
||||
}
|
||||
}
|
|
@ -0,0 +1,266 @@
|
|||
import Foundation
|
||||
import MSAL
|
||||
|
||||
public typealias TokenAcquiredHandler = (_ accountUPN: String, _ accessToken: String?, _ error: String?) -> Void
|
||||
|
||||
@objc(TokenBroker)
|
||||
public final class TokenBroker: NSObject {
|
||||
enum Constants {
|
||||
// Source: https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens
|
||||
static let MicrosoftAccountTenant = "9188040d-6c67-4c5b-b112-36a304b66dad"
|
||||
static let RedirectURI = "msauth.\(Bundle.main.bundleIdentifier ?? "")://auth"
|
||||
}
|
||||
|
||||
@objc(sharedBroker)
|
||||
public static let shared = TokenBroker()
|
||||
|
||||
@objc
|
||||
public var currentAccount: Account?
|
||||
|
||||
private let condition = NSCondition()
|
||||
private let dispatchQueue = DispatchQueue(label: "com.microsoft.ReactTestApp-MSAL.TokenBroker")
|
||||
|
||||
private lazy var config = Config.load()
|
||||
|
||||
private var _publicClientApplication: MSALPublicClientApplication?
|
||||
|
||||
private var publicClientApplication: MSALPublicClientApplication? {
|
||||
if _publicClientApplication == nil {
|
||||
do {
|
||||
_publicClientApplication = try MSALPublicClientApplication(
|
||||
configuration: MSALPublicClientApplicationConfig(
|
||||
clientId: config.clientId,
|
||||
redirectUri: Constants.RedirectURI,
|
||||
authority: nil
|
||||
)
|
||||
)
|
||||
} catch {
|
||||
NSLog("Failed to instantiate MSALPublicClientApplication: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
return _publicClientApplication
|
||||
}
|
||||
|
||||
@objc
|
||||
public func acquireToken(
|
||||
scopes: [String],
|
||||
sender: RTAViewController,
|
||||
onTokenAcquired: @escaping TokenAcquiredHandler
|
||||
) {
|
||||
guard let currentAccount = currentAccount else {
|
||||
onTokenAcquired("", nil, "No current account")
|
||||
return
|
||||
}
|
||||
|
||||
acquireToken(
|
||||
scopes: scopes,
|
||||
userPrincipalName: currentAccount.userPrincipalName,
|
||||
accountType: currentAccount.accountType,
|
||||
sender: sender,
|
||||
onTokenAcquired: onTokenAcquired
|
||||
)
|
||||
}
|
||||
|
||||
@objc
|
||||
public func acquireToken(
|
||||
scopes: [String],
|
||||
userPrincipalName: String?,
|
||||
accountType: AccountType,
|
||||
sender: RTAViewController,
|
||||
onTokenAcquired: @escaping TokenAcquiredHandler
|
||||
) {
|
||||
dispatchQueue.async {
|
||||
self.acquireTokenSilent(
|
||||
scopes: scopes,
|
||||
userPrincipalName: userPrincipalName,
|
||||
accountType: accountType,
|
||||
sender: sender,
|
||||
onTokenAcquired: onTokenAcquired
|
||||
)
|
||||
self.condition.wait()
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public func allAccounts() -> [Account] {
|
||||
var allAccounts: [Account] = []
|
||||
if let accounts = try? publicClientApplication?.allAccounts() {
|
||||
accounts.forEach {
|
||||
guard let username = $0.username,
|
||||
let issuer = $0.accountClaims?["iss"] as? String
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
let accountType = AccountType.from(issuer: issuer)
|
||||
let account = Account(userPrincipalName: username, accountType: accountType)
|
||||
allAccounts.append(account)
|
||||
}
|
||||
}
|
||||
return allAccounts
|
||||
}
|
||||
|
||||
@objc
|
||||
public func removeAllAccounts(sender: RTAViewController) {
|
||||
defer {
|
||||
currentAccount = nil
|
||||
}
|
||||
|
||||
guard let application = publicClientApplication,
|
||||
let accounts = try? application.allAccounts()
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
let completion = { (_: Bool, _: Error?) in }
|
||||
accounts.forEach {
|
||||
signOut(account: $0, sender: sender, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public func signOut(
|
||||
sender: RTAViewController,
|
||||
completion: @escaping (_ success: Bool, _ error: Error?) -> Void
|
||||
) {
|
||||
defer {
|
||||
currentAccount = nil
|
||||
}
|
||||
|
||||
guard let username = currentAccount?.userPrincipalName,
|
||||
let account = try? publicClientApplication?.account(forUsername: username)
|
||||
else {
|
||||
completion(true, nil)
|
||||
return
|
||||
}
|
||||
|
||||
signOut(account: account, sender: sender, completion: completion)
|
||||
}
|
||||
|
||||
private func signOut(
|
||||
account: MSALAccount,
|
||||
sender: RTAViewController,
|
||||
completion: @escaping (_ success: Bool, _ error: Error?) -> Void
|
||||
) {
|
||||
guard let application = publicClientApplication else {
|
||||
completion(true, nil)
|
||||
return
|
||||
}
|
||||
|
||||
let webviewParameters = MSALWebviewParameters(authPresentationViewController: sender)
|
||||
let signoutParameters = MSALSignoutParameters(webviewParameters: webviewParameters)
|
||||
signoutParameters.wipeAccount = true
|
||||
|
||||
application.signout(with: account, signoutParameters: signoutParameters) { success, error in
|
||||
if success {
|
||||
try? application.remove(account)
|
||||
}
|
||||
completion(success, error)
|
||||
}
|
||||
}
|
||||
|
||||
private func acquireTokenInteractive(
|
||||
scopes: [String],
|
||||
userPrincipalName _: String?,
|
||||
accountType: AccountType,
|
||||
sender: RTAViewController,
|
||||
onTokenAcquired: @escaping TokenAcquiredHandler
|
||||
) {
|
||||
guard let application = publicClientApplication else {
|
||||
return
|
||||
}
|
||||
|
||||
let parameters = MSALInteractiveTokenParameters(
|
||||
scopes: scopes,
|
||||
webviewParameters: MSALWebviewParameters(authPresentationViewController: sender)
|
||||
)
|
||||
parameters.authority = try? MSALAuthority(url: accountType.authority)
|
||||
parameters.promptType = .selectAccount
|
||||
|
||||
DispatchQueue.main.async {
|
||||
application.acquireToken(with: parameters) { result, error in
|
||||
self.condition.signal()
|
||||
|
||||
let username = result?.account.username ?? ""
|
||||
let accessToken = result?.accessToken
|
||||
onTokenAcquired(username, accessToken, error?.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func acquireTokenSilent(
|
||||
scopes: [String],
|
||||
userPrincipalName: String?,
|
||||
accountType: AccountType,
|
||||
sender: RTAViewController,
|
||||
onTokenAcquired: @escaping TokenAcquiredHandler
|
||||
) {
|
||||
guard let application = publicClientApplication else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let userPrincipalName = userPrincipalName,
|
||||
let cachedAccount = try? application.account(forUsername: userPrincipalName)
|
||||
else {
|
||||
acquireTokenInteractive(
|
||||
scopes: scopes,
|
||||
userPrincipalName: nil,
|
||||
accountType: accountType,
|
||||
sender: sender,
|
||||
onTokenAcquired: onTokenAcquired
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
let parameters = MSALSilentTokenParameters(scopes: scopes, account: cachedAccount)
|
||||
parameters.authority = try? MSALAuthority(url: accountType.authority)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
application.acquireTokenSilent(with: parameters) { result, error in
|
||||
if let error = error as NSError? {
|
||||
if error.domain == MSALErrorDomain, error.code == MSALError.interactionRequired.rawValue {
|
||||
self.acquireTokenInteractive(
|
||||
scopes: scopes,
|
||||
userPrincipalName: userPrincipalName,
|
||||
accountType: accountType,
|
||||
sender: sender,
|
||||
onTokenAcquired: onTokenAcquired
|
||||
)
|
||||
} else {
|
||||
self.condition.signal()
|
||||
|
||||
// Handle "Cannot start ASWebAuthenticationSession
|
||||
// without providing presentation context. Set
|
||||
// presentationContextProvider before calling
|
||||
// -start." This can occur while the test app is
|
||||
// navigating to the initial component.
|
||||
if error.isMissingPresentationContextError() {
|
||||
self.acquireToken(
|
||||
scopes: scopes,
|
||||
userPrincipalName: userPrincipalName,
|
||||
accountType: accountType,
|
||||
sender: sender,
|
||||
onTokenAcquired: onTokenAcquired
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
onTokenAcquired("", nil, error.localizedDescription)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.condition.signal()
|
||||
onTokenAcquired(userPrincipalName, result?.accessToken, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NSError {
|
||||
func isMissingPresentationContextError() -> Bool {
|
||||
let domain = "com.apple.AuthenticationServices.WebAuthenticationSession"
|
||||
return self.domain == domain && code == 2
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "@rnx-kit/react-native-test-app-msal",
|
||||
"version": "0.0.1",
|
||||
"description": "Microsoft Authentication Library (MSAL) module for react-native-test-app",
|
||||
"homepage": "https://github.com/microsoft/rnx-kit/tree/main/packages/react-native-test-app-msal#readme",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Microsoft Open Source",
|
||||
"email": "microsoftopensource@users.noreply.github.com"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/microsoft/rnx-kit.git",
|
||||
"directory": "packages/react-native-test-app-msal"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "echo Build done.",
|
||||
"format": "echo Format done.",
|
||||
"format:swift": "swiftformat --swiftversion 5.5 ios",
|
||||
"lint": "echo Lint done.",
|
||||
"lint:swift": "swiftlint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-native-test-app": ">=0.9.8"
|
||||
}
|
||||
}
|
|
@ -5,6 +5,9 @@
|
|||
{
|
||||
"appKey": "SampleCrossApp",
|
||||
"displayName": "SampleCrossApp"
|
||||
},
|
||||
{
|
||||
"appKey": "MicrosoftAccounts"
|
||||
}
|
||||
],
|
||||
"resources": {
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
platform :ios, '14.0'
|
||||
require_relative '../../../node_modules/react-native-test-app/test_app'
|
||||
|
||||
workspace 'SampleCrossApp.xcworkspace'
|
||||
|
||||
use_flipper! false
|
||||
use_test_app!
|
||||
use_test_app! do |target|
|
||||
target.app do
|
||||
pod 'MSAL', :modular_headers => true
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,6 +11,9 @@ PODS:
|
|||
- ReactCommon/turbomodule/core (= 0.66.1)
|
||||
- fmt (6.2.1)
|
||||
- glog (0.3.5)
|
||||
- MSAL (1.1.22):
|
||||
- MSAL/app-lib (= 1.1.22)
|
||||
- MSAL/app-lib (1.1.22)
|
||||
- QRCodeReader.swift (10.1.0)
|
||||
- RCT-Folly (2021.06.28.00-v2):
|
||||
- boost
|
||||
|
@ -275,9 +278,11 @@ PODS:
|
|||
- React-jsi (= 0.66.1)
|
||||
- React-logger (= 0.66.1)
|
||||
- React-perflogger (= 0.66.1)
|
||||
- ReactTestApp-DevSupport (0.9.8):
|
||||
- ReactTestApp-DevSupport (0.9.11):
|
||||
- React-Core
|
||||
- React-jsi
|
||||
- ReactTestApp-MSAL (0.0.1):
|
||||
- MSAL
|
||||
- ReactTestApp-Resources (1.0.0-dev)
|
||||
- SwiftLint (0.44.0)
|
||||
- Yoga (1.14.0)
|
||||
|
@ -288,6 +293,7 @@ DEPENDENCIES:
|
|||
- FBLazyVector (from `../../../node_modules/react-native/Libraries/FBLazyVector`)
|
||||
- FBReactNativeSpec (from `../../../node_modules/react-native/React/FBReactNativeSpec`)
|
||||
- glog (from `../../../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
||||
- MSAL
|
||||
- QRCodeReader.swift
|
||||
- RCT-Folly (from `../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
||||
- RCTRequired (from `../../../node_modules/react-native/Libraries/RCTRequired`)
|
||||
|
@ -316,6 +322,7 @@ DEPENDENCIES:
|
|||
- React-runtimeexecutor (from `../../../node_modules/react-native/ReactCommon/runtimeexecutor`)
|
||||
- ReactCommon/turbomodule/core (from `../../../node_modules/react-native/ReactCommon`)
|
||||
- ReactTestApp-DevSupport (from `../../../node_modules/react-native-test-app`)
|
||||
- ReactTestApp-MSAL (from `../../react-native-test-app-msal`)
|
||||
- ReactTestApp-Resources (from `..`)
|
||||
- SwiftLint
|
||||
- Yoga (from `../../../node_modules/react-native/ReactCommon/yoga`)
|
||||
|
@ -323,6 +330,7 @@ DEPENDENCIES:
|
|||
SPEC REPOS:
|
||||
trunk:
|
||||
- fmt
|
||||
- MSAL
|
||||
- QRCodeReader.swift
|
||||
- SwiftLint
|
||||
|
||||
|
@ -387,6 +395,8 @@ EXTERNAL SOURCES:
|
|||
:path: "../../../node_modules/react-native/ReactCommon"
|
||||
ReactTestApp-DevSupport:
|
||||
:path: "../../../node_modules/react-native-test-app"
|
||||
ReactTestApp-MSAL:
|
||||
:path: "../../react-native-test-app-msal"
|
||||
ReactTestApp-Resources:
|
||||
:path: ".."
|
||||
Yoga:
|
||||
|
@ -399,6 +409,7 @@ SPEC CHECKSUMS:
|
|||
FBReactNativeSpec: 74c869e2cffa2ffec685cd1bac6788c021da6005
|
||||
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
|
||||
glog: 5337263514dd6f09803962437687240c5dc39aa4
|
||||
MSAL: 0d88c5430e0ffb8863f41e9f45248981a38a7610
|
||||
QRCodeReader.swift: 373a389fe9a22d513c879a32a6f647c58f4ef572
|
||||
RCT-Folly: a21c126816d8025b547704b777a2ba552f3d9fa9
|
||||
RCTRequired: 3cc065b52aa18db729268b9bd78a2feffb4d0f91
|
||||
|
@ -424,11 +435,12 @@ SPEC CHECKSUMS:
|
|||
React-RCTVibration: 6600b5eed7c0fda4a433fa1198d1cb2690151791
|
||||
React-runtimeexecutor: 33a949a51bec5f8a3c9e8d8092deb259600d761e
|
||||
ReactCommon: 620442811dc6f707b4bf5e3b27d4f19c12d5a821
|
||||
ReactTestApp-DevSupport: c0546bccf7c0fb5ec7102a5af4cc6d20359d0154
|
||||
ReactTestApp-DevSupport: 01c78db18948245da37893bf6390f7fc7459617b
|
||||
ReactTestApp-MSAL: e957be17b0f9419113a1f521d35f70b36440bf2e
|
||||
ReactTestApp-Resources: 74a1cf509f4e7962b16361ea4e73cba3648fff5d
|
||||
SwiftLint: e96c0a8c770c7ebbc4d36c55baf9096bb65c4584
|
||||
Yoga: 2b4a01651f42a32f82e6cef3830a3ba48088237f
|
||||
|
||||
PODFILE CHECKSUM: 71b08e582fb06a8ccbdbaed0cfe518f40b10bffc
|
||||
PODFILE CHECKSUM: d42888eb03ca7eac15b23817b1a07f9b4264db11
|
||||
|
||||
COCOAPODS: 1.11.2
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
"@rnx-kit/metro-serializer": "*",
|
||||
"@rnx-kit/metro-serializer-esbuild": "*",
|
||||
"@rnx-kit/metro-swc-worker": "*",
|
||||
"@rnx-kit/react-native-test-app-msal": "*",
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/react-native": "^0.64.0",
|
||||
"metro-react-native-babel-preset": "^0.66.2",
|
||||
|
|
Загрузка…
Ссылка в новой задаче