@nativelement/capacitor-msal-entra 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/NativelementCapacitorMsalEntra.podspec +18 -0
- package/README.md +460 -2
- package/android/build.gradle +55 -0
- package/android/src/main/java/io/nativelement/msalentra/MsalAuthPlugin.kt +273 -0
- package/android/src/main/res/raw/msal_config.json +14 -0
- package/dist/esm/definitions.d.ts +93 -0
- package/dist/esm/definitions.js +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/web.d.ts +41 -0
- package/dist/esm/web.js +187 -0
- package/dist/plugin.cjs.js +202 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +204 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Plugin/MsalAuthPlugin.swift +204 -0
- package/ios/Plugin/msal_config.json +5 -0
- package/package.json +68 -4
- package/web/msal_config.json +6 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
import MSAL
|
|
4
|
+
|
|
5
|
+
/// Wraps the MSAL iOS SDK so the Ionic/Angular layer only ever deals with
|
|
6
|
+
/// plain JS objects and promises.
|
|
7
|
+
///
|
|
8
|
+
/// Requires the MSAL pod (pod 'MSAL', '~> 1.2') and an entry in Info.plist
|
|
9
|
+
/// registering the CFBundleURLSchemes redirect URI, plus LSApplicationQueriesSchemes
|
|
10
|
+
/// entries for "msauthv2" / "msauthv3" if you want broker support.
|
|
11
|
+
@objc(MsalAuthPlugin)
|
|
12
|
+
public class MsalAuthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
13
|
+
public let identifier = "MsalAuthPlugin"
|
|
14
|
+
public let jsName = "MsalAuth"
|
|
15
|
+
public let pluginMethods: [CAPPluginMethod] = [
|
|
16
|
+
CAPPluginMethod(name: "signIn", returnType: CAPPluginReturnPromise),
|
|
17
|
+
CAPPluginMethod(name: "acquireTokenSilent", returnType: CAPPluginReturnPromise),
|
|
18
|
+
CAPPluginMethod(name: "getAccount", returnType: CAPPluginReturnPromise),
|
|
19
|
+
CAPPluginMethod(name: "signOut", returnType: CAPPluginReturnPromise)
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
private var application: MSALPublicClientApplication?
|
|
23
|
+
|
|
24
|
+
override public func load() {
|
|
25
|
+
NSLog("MsalAuthPlugin: load() called")
|
|
26
|
+
guard
|
|
27
|
+
let path = Bundle.main.path(forResource: "msal_config", ofType: "json"),
|
|
28
|
+
let data = try? Data(contentsOf: URL(fileURLWithPath: path)),
|
|
29
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
30
|
+
let clientId = json["client_id"] as? String,
|
|
31
|
+
let authorityUrlString = json["authority_url"] as? String,
|
|
32
|
+
let redirectUri = json["redirect_uri"] as? String,
|
|
33
|
+
let authorityUrl = URL(string: authorityUrlString)
|
|
34
|
+
else {
|
|
35
|
+
NSLog("MsalAuthPlugin: failed to load msal_config.json")
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
NSLog("MsalAuthPlugin: parsed config, clientId=%@ authority=%@ redirectUri=%@", clientId, authorityUrlString, redirectUri)
|
|
39
|
+
|
|
40
|
+
do {
|
|
41
|
+
let authority = try MSALCIAMAuthority(url: authorityUrl)
|
|
42
|
+
let config = MSALPublicClientApplicationConfig(
|
|
43
|
+
clientId: clientId,
|
|
44
|
+
redirectUri: redirectUri,
|
|
45
|
+
authority: authority
|
|
46
|
+
)
|
|
47
|
+
application = try MSALPublicClientApplication(configuration: config)
|
|
48
|
+
NSLog("MsalAuthPlugin: MSALPublicClientApplication initialized successfully")
|
|
49
|
+
} catch {
|
|
50
|
+
NSLog("MsalAuthPlugin: failed to init MSAL - %@", String(describing: error))
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@objc func signIn(_ call: CAPPluginCall) {
|
|
55
|
+
NSLog("MsalAuthPlugin: signIn() called")
|
|
56
|
+
// 'openid', 'profile' and 'offline_access' are reserved scopes MSAL
|
|
57
|
+
// adds automatically - callers should only pass scopes for a specific
|
|
58
|
+
// protected resource here, or none at all for a basic sign-in.
|
|
59
|
+
let scopes = call.getArray("scopes", String.self) ?? []
|
|
60
|
+
let domainHint = call.getString("domainHint")
|
|
61
|
+
guard let application = application else {
|
|
62
|
+
NSLog("MsalAuthPlugin: signIn() rejected - MSAL not initialized")
|
|
63
|
+
call.reject("MSAL not initialized", "NOT_INITIALIZED")
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
guard let viewController = bridge?.viewController else {
|
|
67
|
+
NSLog("MsalAuthPlugin: signIn() rejected - no view controller")
|
|
68
|
+
call.reject("No view controller to present sign-in from")
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
NSLog("MsalAuthPlugin: signIn() guards passed, scopes=%@, domainHint=%@, dispatching acquireToken", scopes, domainHint ?? "nil")
|
|
72
|
+
|
|
73
|
+
DispatchQueue.main.async {
|
|
74
|
+
NSLog("MsalAuthPlugin: on main queue, building webParameters")
|
|
75
|
+
let webParameters = MSALWebviewParameters(authPresentationViewController: viewController)
|
|
76
|
+
let interactiveParameters = MSALInteractiveTokenParameters(scopes: scopes, webviewParameters: webParameters)
|
|
77
|
+
if let domainHint = domainHint, !domainHint.isEmpty {
|
|
78
|
+
interactiveParameters.extraQueryParameters = ["domain_hint": domainHint]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
NSLog("MsalAuthPlugin: calling application.acquireToken(with:)")
|
|
82
|
+
application.acquireToken(with: interactiveParameters) { result, error in
|
|
83
|
+
NSLog("MsalAuthPlugin: acquireToken completion invoked, error=%@, result=%@", String(describing: error), String(describing: result))
|
|
84
|
+
if let error = error {
|
|
85
|
+
call.reject(error.localizedDescription, self.errorCode(for: error), error)
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
guard let result = result else {
|
|
89
|
+
call.reject("No result returned from MSAL")
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
call.resolve(self.toResultJson(result))
|
|
93
|
+
}
|
|
94
|
+
NSLog("MsalAuthPlugin: acquireToken(with:) call returned (async, awaiting completion)")
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@objc func acquireTokenSilent(_ call: CAPPluginCall) {
|
|
99
|
+
let scopes = call.getArray("scopes", String.self) ?? []
|
|
100
|
+
guard let application = application else {
|
|
101
|
+
call.reject("MSAL not initialized", "NOT_INITIALIZED")
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
do {
|
|
106
|
+
guard let account = try application.allAccounts().first else {
|
|
107
|
+
call.reject("No cached account, call signIn() first", "UI_REQUIRED")
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
let silentParameters = MSALSilentTokenParameters(scopes: scopes, account: account)
|
|
111
|
+
application.acquireTokenSilent(with: silentParameters) { result, error in
|
|
112
|
+
if let error = error as NSError? {
|
|
113
|
+
if error.domain == MSALErrorDomain,
|
|
114
|
+
error.code == MSALError.interactionRequired.rawValue {
|
|
115
|
+
call.reject("Interactive sign-in required", "UI_REQUIRED", error)
|
|
116
|
+
} else {
|
|
117
|
+
call.reject(error.localizedDescription, "SILENT_FAILED", error)
|
|
118
|
+
}
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
guard let result = result else {
|
|
122
|
+
call.reject("No result returned from MSAL")
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
call.resolve(self.toResultJson(result))
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
call.reject(error.localizedDescription, "SILENT_FAILED", error)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@objc func getAccount(_ call: CAPPluginCall) {
|
|
133
|
+
guard let application = application else {
|
|
134
|
+
NSLog("MsalAuthPlugin: getAccount() - application is nil")
|
|
135
|
+
call.resolve(["value": NSNull()])
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
do {
|
|
139
|
+
let accounts = try application.allAccounts()
|
|
140
|
+
NSLog("MsalAuthPlugin: getAccount() - allAccounts() returned %d account(s): %@",
|
|
141
|
+
accounts.count, accounts.map { "\($0.identifier ?? "?") / \($0.username ?? "?")" })
|
|
142
|
+
guard let account = accounts.first else {
|
|
143
|
+
call.resolve(["value": NSNull()])
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
call.resolve([
|
|
147
|
+
"value": [
|
|
148
|
+
"accountId": account.identifier ?? "",
|
|
149
|
+
"username": account.username ?? NSNull(),
|
|
150
|
+
"idTokenClaims": account.accountClaims ?? [:]
|
|
151
|
+
]
|
|
152
|
+
])
|
|
153
|
+
} catch {
|
|
154
|
+
NSLog("MsalAuthPlugin: getAccount() - allAccounts() threw %@", String(describing: error))
|
|
155
|
+
call.reject(error.localizedDescription, "GET_ACCOUNT_FAILED", error)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@objc func signOut(_ call: CAPPluginCall) {
|
|
160
|
+
guard let application = application else {
|
|
161
|
+
NSLog("MsalAuthPlugin: signOut() - application is nil")
|
|
162
|
+
call.resolve()
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
do {
|
|
166
|
+
// Remove every cached account, not just the first - MSAL's
|
|
167
|
+
// Keychain-backed cache can accumulate more than one account
|
|
168
|
+
// over time (e.g. testing with different sign-ins), and
|
|
169
|
+
// signOut() must guarantee no account remains afterward, not
|
|
170
|
+
// just remove whichever one happens to be first in the list.
|
|
171
|
+
let accounts = try application.allAccounts()
|
|
172
|
+
NSLog("MsalAuthPlugin: signOut() - allAccounts() found %d account(s) to remove: %@",
|
|
173
|
+
accounts.count, accounts.map { "\($0.identifier ?? "?") / \($0.username ?? "?")" })
|
|
174
|
+
for account in accounts {
|
|
175
|
+
try application.remove(account)
|
|
176
|
+
NSLog("MsalAuthPlugin: signOut() - removed account %@", account.identifier ?? "?")
|
|
177
|
+
}
|
|
178
|
+
let remaining = try application.allAccounts()
|
|
179
|
+
NSLog("MsalAuthPlugin: signOut() - after removal, allAccounts() returns %d account(s)", remaining.count)
|
|
180
|
+
call.resolve()
|
|
181
|
+
} catch {
|
|
182
|
+
NSLog("MsalAuthPlugin: signOut() - threw %@", String(describing: error))
|
|
183
|
+
call.reject(error.localizedDescription, "SIGN_OUT_FAILED", error)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private func toResultJson(_ result: MSALResult) -> [String: Any] {
|
|
188
|
+
return [
|
|
189
|
+
"accessToken": result.accessToken,
|
|
190
|
+
"idToken": result.idToken ?? "",
|
|
191
|
+
"expiresOn": (result.expiresOn?.timeIntervalSince1970 ?? 0) * 1000,
|
|
192
|
+
"accountId": result.account.identifier ?? "",
|
|
193
|
+
"scopes": result.scopes
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private func errorCode(for error: Error) -> String {
|
|
198
|
+
let nsError = error as NSError
|
|
199
|
+
if nsError.domain == MSALErrorDomain, nsError.code == MSALError.userCanceled.rawValue {
|
|
200
|
+
return "USER_CANCELLED"
|
|
201
|
+
}
|
|
202
|
+
return "SIGN_IN_FAILED"
|
|
203
|
+
}
|
|
204
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,70 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nativelement/capacitor-msal-entra",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Capacitor plugin wrapping native MSAL SDKs (Android/iOS) and msal-browser (web) for Microsoft Entra External ID authentication",
|
|
5
|
+
"main": "dist/plugin.cjs.js",
|
|
6
|
+
"module": "dist/esm/index.js",
|
|
7
|
+
"types": "dist/esm/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"android/src/main/",
|
|
10
|
+
"android/build.gradle",
|
|
11
|
+
"dist/",
|
|
12
|
+
"ios/Plugin/",
|
|
13
|
+
"web/",
|
|
14
|
+
"NativelementCapacitorMsalEntra.podspec",
|
|
15
|
+
"LICENSE",
|
|
16
|
+
"CHANGELOG.md"
|
|
17
|
+
],
|
|
18
|
+
"author": "Nativelement",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"scripts": {
|
|
21
|
+
"clean": "rm -rf ./dist",
|
|
22
|
+
"build": "npm run clean && tsc && rollup -c rollup.config.js",
|
|
23
|
+
"watch": "tsc --watch",
|
|
24
|
+
"prepublishOnly": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@azure/msal-browser": "^5.22.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@capacitor/android": "^7.0.0",
|
|
31
|
+
"@capacitor/core": "^7.0.0",
|
|
32
|
+
"@capacitor/ios": "^7.0.0",
|
|
33
|
+
"rollup": "^4.9.0",
|
|
34
|
+
"typescript": "^5.4.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@capacitor/core": ">=6.0.0"
|
|
38
|
+
},
|
|
39
|
+
"capacitor": {
|
|
40
|
+
"ios": {
|
|
41
|
+
"src": "ios"
|
|
42
|
+
},
|
|
43
|
+
"android": {
|
|
44
|
+
"src": "android"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"capacitor",
|
|
49
|
+
"plugin",
|
|
50
|
+
"msal",
|
|
51
|
+
"entra",
|
|
52
|
+
"entra-external-id",
|
|
53
|
+
"azure-ad",
|
|
54
|
+
"ciam",
|
|
55
|
+
"authentication",
|
|
56
|
+
"ios",
|
|
57
|
+
"android"
|
|
58
|
+
],
|
|
59
|
+
"repository": {
|
|
60
|
+
"type": "git",
|
|
61
|
+
"url": "git+https://github.com/NativElement/capacitor-msal-entra.git"
|
|
62
|
+
},
|
|
63
|
+
"bugs": {
|
|
64
|
+
"url": "https://github.com/NativElement/capacitor-msal-entra/issues"
|
|
65
|
+
},
|
|
66
|
+
"homepage": "https://github.com/NativElement/capacitor-msal-entra#readme",
|
|
67
|
+
"publishConfig": {
|
|
68
|
+
"access": "public"
|
|
69
|
+
}
|
|
70
|
+
}
|