@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.
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "client_id": "<IOS_APP_CLIENT_ID>",
3
+ "redirect_uri": "msauth.com.example.myapp://auth",
4
+ "authority_url": "https://<your-tenant-subdomain>.ciamlogin.com/<your-tenant-subdomain>.onmicrosoft.com/"
5
+ }
package/package.json CHANGED
@@ -1,6 +1,70 @@
1
1
  {
2
2
  "name": "@nativelement/capacitor-msal-entra",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "client_id": "<WEB_APP_CLIENT_ID>",
3
+ "authority_url": "https://<your-tenant-subdomain>.ciamlogin.com/<your-tenant-subdomain>.onmicrosoft.com/",
4
+ "redirect_uri": "https://<your-app-host>/",
5
+ "known_authorities": []
6
+ }