Skip to content

Commit 4adde09

Browse files
committed
update snippets
1 parent 36d335e commit 4adde09

2 files changed

Lines changed: 58 additions & 5 deletions

File tree

snippet-manifest.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ scenarios:
9595
value: "{{SCOPES}}"
9696
frameworks:
9797
- android
98+
- ios
9899
- react-native
99100
spa_login_pkce:
100101
app_type: single_page

snippets.json

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,18 @@
2121
{
2222
"step": 3,
2323
"description": "Open Chrome Custom Tabs, run Auth Code + PKCE, and receive tokens",
24-
"code": " suspend fun signIn(): Intent? {\n _error.value = null\n _expired.value = false\n val issuerJavaUri = AuthConfig.issuer\n ?: run { _error.value = \"CIAM_ISSUER_HOST is missing — fill in local.properties\"; return null }\n if (AuthConfig.clientId.isEmpty()) {\n _error.value = \"CIAM_CLIENT_ID is missing — fill in local.properties\"; return null\n }\n val redirect = AuthConfig.redirectUri\n ?: run { _error.value = \"CIAM_REDIRECT_SCHEME is missing — fill in local.properties\"; return null }\n return try {\n val issuerAndroidUri = android.net.Uri.parse(issuerJavaUri.toString())\n val config = discoverConfiguration(issuerAndroidUri)\n val request = AuthorizationRequest.Builder(\n config,\n AuthConfig.clientId,\n ResponseTypeValues.CODE,\n redirect,\n )\n .setScopes(AuthConfig.scopes)\n .build()\n pendingAuthState = AuthState(config)\n authService.getAuthorizationRequestIntent(request)\n } catch (e: Throwable) {\n _error.value = e.localizedMessage ?: \"Authorization failed\"\n null\n }\n }",
24+
"code": " suspend fun signIn(): Intent? {\n _error.value = null\n _expired.value = false\n val issuerJavaUri = AuthConfig.issuer\n ?: run { _error.value = \"CIAM_ISSUER_HOST is missing — fill in local.properties\"; return null }\n if (AuthConfig.clientId.isEmpty()) {\n _error.value = \"CIAM_CLIENT_ID is missing — fill in local.properties\"; return null\n }\n val redirect = AuthConfig.redirectUri\n ?: run { _error.value = \"CIAM_REDIRECT_SCHEME is missing — fill in local.properties\"; return null }\n return try {\n val issuerAndroidUri = android.net.Uri.parse(issuerJavaUri.toString())\n val config = discoverConfiguration(issuerAndroidUri)\n val request = AuthorizationRequest.Builder(\n config,\n AuthConfig.clientId,\n ResponseTypeValues.CODE,\n redirect,\n )\n .setScopes(AuthConfig.scopes)\n .build()\n val intent = authService.getAuthorizationRequestIntent(request)\n pendingAuthState = AuthState(config)\n intent\n } catch (e: Exception) {\n _error.value = e.localizedMessage ?: \"Authorization failed\"\n null\n }\n }",
2525
"file": "app/src/main/java/com/secureauth/quickstart/AuthViewModel.kt",
2626
"lang": "kotlin",
27-
"lines": "79-108"
27+
"lines": "79-109"
2828
},
2929
{
3030
"step": 4,
3131
"description": "Revoke the access token at the IdP and clear local auth state",
32-
"code": " suspend fun signOut() {\n _state.value?.lastTokenResponse?.accessToken?.let { token ->\n try { revokeToken(token) } catch (_: Throwable) { /* best-effort */ }\n }\n _state.value = null\n _expired.value = false\n _error.value = null\n expiryJob?.cancel()\n }",
32+
"code": " suspend fun signOut() {\n _state.value?.lastTokenResponse?.accessToken?.let { token ->\n try { revokeToken(token) } catch (_: Exception) { /* best-effort */ }\n }\n _state.value = null\n _expired.value = false\n _error.value = null\n expiryJob?.cancel()\n }",
3333
"file": "app/src/main/java/com/secureauth/quickstart/AuthViewModel.kt",
3434
"lang": "kotlin",
35-
"lines": "136-146"
35+
"lines": "138-148"
3636
}
3737
],
3838
"framework": "android",
@@ -81,7 +81,7 @@
8181
],
8282
"framework": "ios",
8383
"lib": "AppAuth-iOS",
84-
"lib_version": "1.7.6",
84+
"lib_version": "2.0.0",
8585
"docs_url": "https://github.com/openid/AppAuth-iOS",
8686
"install": "",
8787
"repo_path": "samples/ios/login-pkce",
@@ -186,6 +186,58 @@
186186
"run_command": "./gradlew :app:installDebug",
187187
"callout": "Requires refresh_token grant type enabled in workspace [OAuth settings](workspace-oauth-general) and in the application's [Grant Types](workspace-applications-oauth). Also requires the offline_access scope."
188188
},
189+
"ios": {
190+
"steps": [
191+
{
192+
"step": 1,
193+
"description": "Import AppAuth, Foundation, and Security (the Keychain backend)",
194+
"code": "import AppAuth\nimport Foundation\nimport Security",
195+
"file": "Quickstart/AuthClient.swift",
196+
"lang": "swift",
197+
"lines": "3-7"
198+
},
199+
{
200+
"step": 2,
201+
"description": "Configure the OIDC client — `offline_access` (set in Config.xcconfig) enables refresh tokens",
202+
"code": "private func plist(_ key: String) -> String {\n Bundle.main.object(forInfoDictionaryKey: key) as? String ?? \"\"\n}\n\nstruct AuthConfig {\n static let issuer = AuthConfig.buildIssuerURL(host: plist(\"CIAM_ISSUER_HOST\"),\n path: plist(\"CIAM_ISSUER_PATH\"))\n static let clientId = plist(\"CIAM_CLIENT_ID\")\n static let redirectURI: URL? = {\n let scheme = plist(\"CIAM_REDIRECT_SCHEME\")\n guard !scheme.isEmpty else { return nil }\n return URL(string: \"\\(scheme)://oauthredirect\")\n }()\n static let scopes = plist(\"CIAM_SCOPES\").split(separator: \" \").map(String.init)\n\n static func buildIssuerURL(host: String, path: String) -> URL? {\n guard !host.isEmpty else { return nil }\n let trimmedPath = path.trimmingCharacters(in: CharacterSet(charactersIn: \"/\"))\n let urlString = trimmedPath.isEmpty ? \"https://\\(host)\" : \"https://\\(host)/\\(trimmedPath)\"\n return URL(string: urlString)\n }\n}",
203+
"file": "Quickstart/AuthClient.swift",
204+
"lang": "swift",
205+
"lines": "10-33"
206+
},
207+
{
208+
"step": 3,
209+
"description": "Trigger login with offline_access and capture the refresh token",
210+
"code": " func signIn(presenting: UIViewController) async {\n error = nil\n do {\n guard let issuer = AuthConfig.issuer else {\n throw AuthError.misconfigured(\"CIAM_ISSUER_HOST is missing — fill in Config.xcconfig\")\n }\n guard !AuthConfig.clientId.isEmpty else {\n throw AuthError.misconfigured(\"CIAM_CLIENT_ID is missing — fill in Config.xcconfig\")\n }\n guard let redirectURI = AuthConfig.redirectURI else {\n throw AuthError.misconfigured(\"CIAM_REDIRECT_SCHEME is missing — fill in Config.xcconfig\")\n }\n let config = try await discoverConfiguration(forIssuer: issuer)\n let request = OIDAuthorizationRequest(\n configuration: config,\n clientId: AuthConfig.clientId,\n scopes: AuthConfig.scopes,\n redirectURL: redirectURI,\n responseType: OIDResponseTypeCode,\n additionalParameters: nil\n )\n let authState = try await authStateByPresenting(request: request, presenting: presenting)\n let next = AuthClient.makeTokens(from: authState.lastTokenResponse, fallback: nil)\n tokens = next\n if let refresh = next?.refreshToken {\n persistRefreshToken(refresh)\n }\n scheduleRefreshTimer()\n } catch {\n self.error = error.localizedDescription\n }\n }",
211+
"file": "Quickstart/AuthClient.swift",
212+
"lang": "swift",
213+
"lines": "52-85"
214+
},
215+
{
216+
"step": 4,
217+
"description": "Persist the refresh token in iOS Keychain (kSecClassGenericPassword, kSecAttrAccessibleWhenUnlocked)",
218+
"code": "struct RefreshTokenStore {\n let service: String\n\n init(service: String = \"com.secureauth.quickstart.ios.refresh\") {\n self.service = service\n }\n\n private var account: String { \"refreshToken\" }\n\n func save(_ token: String) throws {\n guard let data = token.data(using: .utf8) else {\n throw RefreshTokenStoreError.unexpectedData\n }\n // Try update first (in case an entry exists), fall back to add. Avoids the\n // \"duplicate item\" error from SecItemAdd when overwriting.\n let updateStatus = SecItemUpdate(matchQuery() as CFDictionary,\n [kSecValueData as String: data] as CFDictionary)\n if updateStatus == errSecSuccess { return }\n if updateStatus != errSecItemNotFound {\n throw RefreshTokenStoreError.osStatus(updateStatus)\n }\n var addQuery = matchQuery()\n addQuery[kSecValueData as String] = data\n addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlocked\n let addStatus = SecItemAdd(addQuery as CFDictionary, nil)\n if addStatus != errSecSuccess {\n throw RefreshTokenStoreError.osStatus(addStatus)\n }\n }\n\n func load() throws -> String? {\n var query = matchQuery()\n query[kSecReturnData as String] = true\n query[kSecMatchLimit as String] = kSecMatchLimitOne\n var result: AnyObject?\n let status = SecItemCopyMatching(query as CFDictionary, &result)\n if status == errSecItemNotFound { return nil }\n if status != errSecSuccess {\n throw RefreshTokenStoreError.osStatus(status)\n }\n guard let data = result as? Data, let token = String(data: data, encoding: .utf8) else {\n throw RefreshTokenStoreError.unexpectedData\n }\n return token\n }\n\n func clear() throws {\n let status = SecItemDelete(matchQuery() as CFDictionary)\n // errSecItemNotFound is fine — clear is idempotent.\n if status != errSecSuccess && status != errSecItemNotFound {\n throw RefreshTokenStoreError.osStatus(status)\n }\n }\n\n /// Common query attributes shared across save/load/clear.\n /// `kSecUseDataProtectionKeychain: true` opts into the modern data-protection\n /// keychain (iOS 13+), which doesn't require a Keychain Sharing entitlement and\n /// works correctly in unit-test bundles run with code signing disabled.\n private func matchQuery() -> [String: Any] {\n return [\n kSecClass as String: kSecClassGenericPassword,\n kSecAttrService as String: service,\n kSecAttrAccount as String: account,\n kSecUseDataProtectionKeychain as String: true,\n ]\n }\n}\n\nenum RefreshTokenStoreError: Error {\n case osStatus(OSStatus)\n case unexpectedData\n}",
219+
"file": "Quickstart/RefreshTokenStore.swift",
220+
"lang": "swift",
221+
"lines": "4-77"
222+
},
223+
{
224+
"step": 5,
225+
"description": "Exchange the refresh token for a new access token; on failure clear local state and require re-login",
226+
"code": " func refreshTokens() async {\n error = nil\n guard let issuer = AuthConfig.issuer else {\n error = \"CIAM_ISSUER_HOST is missing — fill in Config.xcconfig\"\n return\n }\n guard !AuthConfig.clientId.isEmpty else {\n error = \"CIAM_CLIENT_ID is missing — fill in Config.xcconfig\"\n return\n }\n let storedRefresh: String?\n if let inMemory = tokens?.refreshToken, !inMemory.isEmpty {\n storedRefresh = inMemory\n } else {\n storedRefresh = (try? store.load()).flatMap { $0.isEmpty ? nil : $0 }\n }\n guard let stored = storedRefresh else {\n error = \"No refresh token available. Sign in first.\"\n return\n }\n do {\n let config = try await discoverConfiguration(forIssuer: issuer)\n let request = OIDTokenRequest(\n configuration: config,\n grantType: OIDGrantTypeRefreshToken,\n authorizationCode: nil,\n redirectURL: nil,\n clientID: AuthConfig.clientId,\n clientSecret: nil,\n scope: nil,\n refreshToken: stored,\n codeVerifier: nil,\n additionalParameters: nil\n )\n let response = try await performTokenRequest(request)\n let next = AuthClient.makeTokens(from: response, fallback: tokens)\n tokens = next\n if let newRefresh = next?.refreshToken, newRefresh != stored {\n persistRefreshToken(newRefresh)\n }\n scheduleRefreshTimer()\n } catch {\n // Refresh tokens can be revoked or expire — clear local state and force re-login.\n try? store.clear()\n tokens = nil\n refreshTimer?.cancel()\n self.error = \"Refresh failed (\\(error.localizedDescription)). Sign in again.\"\n }\n }",
227+
"file": "Quickstart/AuthClient.swift",
228+
"lang": "swift",
229+
"lines": "88-138"
230+
}
231+
],
232+
"framework": "ios",
233+
"lib": "AppAuth-iOS",
234+
"lib_version": "2.0.0",
235+
"docs_url": "https://github.com/openid/AppAuth-iOS",
236+
"install": "",
237+
"repo_path": "samples/ios/token-refresh",
238+
"run_command": "open Quickstart.xcodeproj",
239+
"callout": "Requires refresh_token grant type enabled in workspace [OAuth settings](workspace-oauth-general) and in the application's [Grant Types](workspace-applications-oauth). Also requires the offline_access scope."
240+
},
189241
"react-native": {
190242
"steps": [
191243
{

0 commit comments

Comments
 (0)