capacitor-oidc 0.0.1-alpha.0

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.
Files changed (44) hide show
  1. package/CapacitorOidc.podspec +17 -0
  2. package/LICENSE +201 -0
  3. package/Package.swift +28 -0
  4. package/README.md +122 -0
  5. package/android/build.gradle +75 -0
  6. package/android/src/main/AndroidManifest.xml +1 -0
  7. package/android/src/main/java/com/jojopirker/capacitor/oidc/CallbackUriMatcher.java +22 -0
  8. package/android/src/main/java/com/jojopirker/capacitor/oidc/CapacitorOidcPlugin.java +240 -0
  9. package/android/src/main/java/com/jojopirker/capacitor/oidc/NativeContract.java +35 -0
  10. package/android/src/main/java/com/jojopirker/capacitor/oidc/StoredSessionV1.java +111 -0
  11. package/android/src/main/java/com/jojopirker/capacitor/oidc/TokenVault.java +209 -0
  12. package/android/src/main/java/com/jojopirker/capacitor/oidc/VaultEntryCodec.java +56 -0
  13. package/dist/esm/capacitor-navigator.d.ts +14 -0
  14. package/dist/esm/capacitor-navigator.js +68 -0
  15. package/dist/esm/capacitor-navigator.js.map +1 -0
  16. package/dist/esm/capacitor-secure-state-store.d.ts +9 -0
  17. package/dist/esm/capacitor-secure-state-store.js +36 -0
  18. package/dist/esm/capacitor-secure-state-store.js.map +1 -0
  19. package/dist/esm/capacitor-user-manager.d.ts +28 -0
  20. package/dist/esm/capacitor-user-manager.js +160 -0
  21. package/dist/esm/capacitor-user-manager.js.map +1 -0
  22. package/dist/esm/definitions.d.ts +62 -0
  23. package/dist/esm/definitions.js +2 -0
  24. package/dist/esm/definitions.js.map +1 -0
  25. package/dist/esm/errors.d.ts +6 -0
  26. package/dist/esm/errors.js +13 -0
  27. package/dist/esm/errors.js.map +1 -0
  28. package/dist/esm/generated/native-contract.d.ts +37 -0
  29. package/dist/esm/generated/native-contract.js +38 -0
  30. package/dist/esm/generated/native-contract.js.map +1 -0
  31. package/dist/esm/index.d.ts +5 -0
  32. package/dist/esm/index.js +5 -0
  33. package/dist/esm/index.js.map +1 -0
  34. package/dist/esm/native.d.ts +2 -0
  35. package/dist/esm/native.js +4 -0
  36. package/dist/esm/native.js.map +1 -0
  37. package/dist/plugin.cjs +304 -0
  38. package/dist/plugin.cjs.map +1 -0
  39. package/ios/Sources/CapacitorOidcPlugin/CapacitorOidcPlugin.swift +177 -0
  40. package/ios/Sources/CapacitorOidcPlugin/NativeContract.generated.swift +39 -0
  41. package/ios/Sources/CapacitorOidcPlugin/StoredSessionV1.swift +35 -0
  42. package/ios/Sources/CapacitorOidcPlugin/TokenVault.swift +132 -0
  43. package/ios/Tests/CapacitorOidcPluginTests/StoredSessionV1Tests.swift +38 -0
  44. package/package.json +85 -0
@@ -0,0 +1,132 @@
1
+ import Foundation
2
+ import Security
3
+
4
+ public enum TokenVaultAccessibility: Sendable {
5
+ case afterFirstUnlockThisDeviceOnly
6
+ case whenUnlockedThisDeviceOnly
7
+
8
+ var keychainValue: CFString {
9
+ switch self {
10
+ case .afterFirstUnlockThisDeviceOnly:
11
+ return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
12
+ case .whenUnlockedThisDeviceOnly:
13
+ return kSecAttrAccessibleWhenUnlockedThisDeviceOnly
14
+ }
15
+ }
16
+ }
17
+
18
+ public enum TokenVaultError: Error {
19
+ case keychain(OSStatus)
20
+ case invalidSession
21
+ }
22
+
23
+ public final class TokenVault: @unchecked Sendable {
24
+ public static let sessionSnapshotKey = "widget-session-v1"
25
+
26
+ private let service: String
27
+ private let accessGroup: String?
28
+ private let accessibility: TokenVaultAccessibility
29
+
30
+ public init(
31
+ service: String = "com.jojopirker.capacitor.oidc",
32
+ accessGroup: String? = nil,
33
+ accessibility: TokenVaultAccessibility = .afterFirstUnlockThisDeviceOnly
34
+ ) {
35
+ self.service = service
36
+ self.accessGroup = accessGroup
37
+ self.accessibility = accessibility
38
+ }
39
+
40
+ public func set(namespace: String, key: String, value: String) throws {
41
+ let data = Data(value.utf8)
42
+ let query = itemQuery(namespace: namespace, key: key)
43
+ let update: [CFString: Any] = [
44
+ kSecValueData: data,
45
+ kSecAttrAccessible: accessibility.keychainValue
46
+ ]
47
+ let status = SecItemUpdate(query as CFDictionary, update as CFDictionary)
48
+ if status == errSecSuccess { return }
49
+ if status != errSecItemNotFound { throw TokenVaultError.keychain(status) }
50
+
51
+ var item = query
52
+ item[kSecValueData as String] = data
53
+ item[kSecAttrAccessible as String] = accessibility.keychainValue
54
+ let addStatus = SecItemAdd(item as CFDictionary, nil)
55
+ guard addStatus == errSecSuccess else { throw TokenVaultError.keychain(addStatus) }
56
+ }
57
+
58
+ public func get(namespace: String, key: String) throws -> String? {
59
+ var query = itemQuery(namespace: namespace, key: key)
60
+ query[kSecReturnData as String] = true
61
+ query[kSecMatchLimit as String] = kSecMatchLimitOne
62
+
63
+ var result: CFTypeRef?
64
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
65
+ if status == errSecItemNotFound { return nil }
66
+ guard status == errSecSuccess, let data = result as? Data else {
67
+ throw TokenVaultError.keychain(status)
68
+ }
69
+ return String(data: data, encoding: .utf8)
70
+ }
71
+
72
+ @discardableResult
73
+ public func remove(namespace: String, key: String) throws -> String? {
74
+ let value = try get(namespace: namespace, key: key)
75
+ let status = SecItemDelete(itemQuery(namespace: namespace, key: key) as CFDictionary)
76
+ if status != errSecSuccess && status != errSecItemNotFound {
77
+ throw TokenVaultError.keychain(status)
78
+ }
79
+ return value
80
+ }
81
+
82
+ public func getAllKeys(namespace: String) throws -> [String] {
83
+ var query = baseQuery()
84
+ query[kSecReturnAttributes as String] = true
85
+ query[kSecMatchLimit as String] = kSecMatchLimitAll
86
+
87
+ var result: CFTypeRef?
88
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
89
+ if status == errSecItemNotFound { return [] }
90
+ guard status == errSecSuccess, let items = result as? [[String: Any]] else {
91
+ throw TokenVaultError.keychain(status)
92
+ }
93
+
94
+ let prefix = "\(namespace)."
95
+ return items.compactMap { item in
96
+ guard let account = item[kSecAttrAccount as String] as? String, account.hasPrefix(prefix) else { return nil }
97
+ return String(account.dropFirst(prefix.count))
98
+ }
99
+ }
100
+
101
+ public func setSessionSnapshot(namespace: String, value: String?) throws {
102
+ if let value {
103
+ try set(namespace: namespace, key: Self.sessionSnapshotKey, value: value)
104
+ } else {
105
+ try remove(namespace: namespace, key: Self.sessionSnapshotKey)
106
+ }
107
+ }
108
+
109
+ public func loadSession(namespace: String) throws -> StoredSessionV1? {
110
+ guard let value = try get(namespace: namespace, key: Self.sessionSnapshotKey),
111
+ let data = value.data(using: .utf8) else { return nil }
112
+ let session = try JSONDecoder().decode(StoredSessionV1.self, from: data)
113
+ guard session.version == 1 else { throw TokenVaultError.invalidSession }
114
+ return session
115
+ }
116
+
117
+ private func baseQuery() -> [String: Any] {
118
+ var query: [String: Any] = [
119
+ kSecClass as String: kSecClassGenericPassword,
120
+ kSecAttrService as String: service,
121
+ kSecAttrSynchronizable as String: false
122
+ ]
123
+ if let accessGroup { query[kSecAttrAccessGroup as String] = accessGroup }
124
+ return query
125
+ }
126
+
127
+ private func itemQuery(namespace: String, key: String) -> [String: Any] {
128
+ var query = baseQuery()
129
+ query[kSecAttrAccount as String] = "\(namespace).\(key)"
130
+ return query
131
+ }
132
+ }
@@ -0,0 +1,38 @@
1
+ import XCTest
2
+ @testable import CapacitorOidcPlugin
3
+
4
+ final class StoredSessionV1Tests: XCTestCase {
5
+ func testDecodesSharedFixture() throws {
6
+ let fixtureURL = URL(fileURLWithPath: #filePath)
7
+ .deletingLastPathComponent()
8
+ .deletingLastPathComponent()
9
+ .deletingLastPathComponent()
10
+ .deletingLastPathComponent()
11
+ .appendingPathComponent("contracts/fixtures/stored-session-v1.json")
12
+ let session = try JSONDecoder().decode(StoredSessionV1.self, from: Data(contentsOf: fixtureURL))
13
+
14
+ XCTAssertEqual(session.version, 1)
15
+ XCTAssertEqual(session.issuer, "https://issuer.example")
16
+ XCTAssertEqual(session.clientId, "mobile")
17
+ XCTAssertEqual(session.accessToken, "access")
18
+ XCTAssertEqual(session.refreshToken, "refresh")
19
+ XCTAssertEqual(session.idToken, "id-token")
20
+ XCTAssertEqual(session.tokenType, "Bearer")
21
+ XCTAssertEqual(session.scope, "openid profile offline_access")
22
+ XCTAssertEqual(session.expiresAt, 1_800_000_000)
23
+ }
24
+
25
+ func testRoundTrip() throws {
26
+ let session = StoredSessionV1(
27
+ issuer: "https://issuer.example",
28
+ clientId: "mobile",
29
+ accessToken: "access",
30
+ refreshToken: "refresh",
31
+ tokenType: "Bearer",
32
+ expiresAt: 1_800_000_000
33
+ )
34
+
35
+ let decoded = try JSONDecoder().decode(StoredSessionV1.self, from: JSONEncoder().encode(session))
36
+ XCTAssertEqual(decoded, session)
37
+ }
38
+ }
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "capacitor-oidc",
3
+ "version": "0.0.1-alpha.0",
4
+ "description": "A minimal Capacitor adapter for standards-based OpenID Connect authentication.",
5
+ "main": "dist/plugin.cjs",
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/Sources",
13
+ "ios/Tests",
14
+ "Package.swift",
15
+ "CapacitorOidc.podspec"
16
+ ],
17
+ "license": "Apache-2.0",
18
+ "author": {
19
+ "name": "Jojo Pirker",
20
+ "email": "j.pirker@gmail.com"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/jojopirker/capacitor-oidc.git"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/jojopirker/capacitor-oidc/issues"
28
+ },
29
+ "homepage": "https://github.com/jojopirker/capacitor-oidc#readme",
30
+ "type": "module",
31
+ "scripts": {
32
+ "build": "npm run clean && tsc && rollup -c rollup.config.mjs",
33
+ "clean": "rimraf dist",
34
+ "check:contract": "node scripts/generate-native-contract.mjs --check",
35
+ "generate:contract": "node scripts/generate-native-contract.mjs",
36
+ "lint": "eslint src --ext ts && prettier --check src scripts contracts package.json README.md SECURITY.md docs",
37
+ "prepack": "npm run build",
38
+ "test": "vitest run",
39
+ "verify": "npm run check:contract && npm run lint && npm run test && npm run build",
40
+ "verify:ios": "xcodebuild -scheme CapacitorOidc -destination generic/platform=iOS CODE_SIGNING_ALLOWED=NO",
41
+ "verify:android": "cd android && ./gradlew clean test assembleDebug"
42
+ },
43
+ "dependencies": {
44
+ "oidc-client-ts": "3.5.0"
45
+ },
46
+ "peerDependencies": {
47
+ "@capacitor/app": "^8.0.0",
48
+ "@capacitor/core": "^8.0.0"
49
+ },
50
+ "devDependencies": {
51
+ "@capacitor/android": "8.5.0",
52
+ "@capacitor/app": "8.1.1",
53
+ "@capacitor/core": "8.5.0",
54
+ "@capacitor/ios": "8.5.0",
55
+ "@ionic/eslint-config": "0.4.0",
56
+ "@ionic/prettier-config": "4.0.0",
57
+ "@types/node": "24.5.2",
58
+ "eslint": "8.57.1",
59
+ "prettier": "3.6.2",
60
+ "rimraf": "6.1.0",
61
+ "rollup": "4.62.5",
62
+ "typescript": "5.9.3",
63
+ "vitest": "3.2.7"
64
+ },
65
+ "prettier": "@ionic/prettier-config",
66
+ "eslintConfig": {
67
+ "extends": "@ionic/eslint-config/recommended",
68
+ "rules": {
69
+ "@typescript-eslint/no-unused-vars": [
70
+ "error",
71
+ {
72
+ "argsIgnorePattern": "^_"
73
+ }
74
+ ]
75
+ }
76
+ },
77
+ "capacitor": {
78
+ "ios": {
79
+ "src": "ios"
80
+ },
81
+ "android": {
82
+ "src": "android"
83
+ }
84
+ }
85
+ }