Open Source Notices
Pinstripe Fleet includes open-source software. This page identifies a modified component, its license, and the corresponding source needed to reproduce Pinstripe's distributed version.
@capgo/capacitor-social-login 8.5.12
This component is licensed under the Mozilla Public License 2.0. Pinstripe modifiesios/Sources/SocialLoginPlugin/AppleProvider.swiftto remove plaintext Apple-token persistence, clear local Apple token/name/subject state on initialization and sign-out, and remove sensitive logging while preserving the authorization-code handoff.
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. The corresponding source is the exact upstream source at commitf6913908f72c482e249f1b206e06b5bbf9e8ea9atogether with the Pinstripe patch reproduced below.
Pinstripe modification source
Apply this patch at the root of the upstream 8.5.12 source tree.
diff --git a/ios/Sources/SocialLoginPlugin/AppleProvider.swift b/ios/Sources/SocialLoginPlugin/AppleProvider.swift
--- a/ios/Sources/SocialLoginPlugin/AppleProvider.swift
+++ b/ios/Sources/SocialLoginPlugin/AppleProvider.swift
@@ -164,75 +164,35 @@
}
self.useProperTokenExchange = useProperTokenExchange
- do {
- try retrieveState()
- } catch {
- print("retrieveState error: \(error)")
- }
+ // Versions before Pinstripe's vendor hardening stored Apple identity,
+ // refresh and access tokens as plaintext JSON in UserDefaults. Never
+ // restore that state; remove it on every provider initialization so an
+ // upgrade also clears any value left by an earlier app build.
+ clearSessionState()
}
func persistState(idToken: String, refreshToken: String, accessToken: String) throws {
- // Create a dictionary to represent the JSON object
- let object: [String: String] = [
- "idToken": idToken,
- "refreshToken": refreshToken,
- "accessToken": accessToken
- ]
-
- // Assign to instance variables
+ // Keep session state in memory only. Pinstripe's proper-token-exchange
+ // flow sends the one-time authorization code to its backend, which
+ // stores the refresh token encrypted. Tokens must not be written to
+ // UserDefaults or emitted to device logs.
self.idToken = idToken
self.refreshToken = refreshToken
self.accessToken = accessToken
-
- // Convert the object to JSON data
- let jsonData = try JSONSerialization.data(withJSONObject: object, options: [])
-
- // Convert JSON data to a string for logging
- if let jsonString = String(data: jsonData, encoding: .utf8) {
- // Log the object
- print("Apple persistState: \(jsonString)")
-
- // Save the JSON string to UserDefaults or use your helper method
- UserDefaults.standard.set(jsonString, forKey: SHARED_PREFERENCE_NAME)
- } else {
- print("Error converting JSON data to String")
- }
- }
-
- func retrieveState() throws {
- // Retrieve the JSON string from persistent storage
- guard let jsonString = UserDefaults.standard.string(forKey: SHARED_PREFERENCE_NAME) else {
- print("No saved state found")
- return
- }
-
- // Convert JSON string to Data
- guard let jsonData = jsonString.data(using: .utf8) else {
- print("Error converting JSON string to Data")
- return
- }
-
- // Parse the JSON data
- guard let object = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: String] else {
- print("Error parsing JSON data")
- return
- }
-
- // Extract tokens
- guard let idToken = object["idToken"],
- let refreshToken = object["refreshToken"],
- let accessToken = object["accessToken"] else {
- print("Error: Missing tokens in retrieved data")
- return
- }
-
- // Assign to instance variables
- self.idToken = idToken
- self.refreshToken = refreshToken
- self.accessToken = accessToken
-
- // Log the retrieved object
- print("Apple retrieveState: \(object)")
+ clearPersistentState()
+ }
+
+ private func clearPersistentState() {
+ UserDefaults.standard.removeObject(forKey: SHARED_PREFERENCE_NAME)
+ UserDefaults.standard.removeObject(forKey: USER_INFO_KEY)
+ }
+
+ private func clearSessionState() {
+ self.idToken = nil
+ self.refreshToken = nil
+ self.accessToken = nil
+ self.completion = nil
+ clearPersistentState()
}
func login(payload: [String: Any], completion: @escaping (Result<AppleProviderResponse, Error>) -> Void) {
@@ -265,20 +225,11 @@
}
func logout(completion: @escaping (Result<Void, Error>) -> Void) {
- // we check only idtoken, because with apple, refresh token MIGHT not be set
- if self.idToken == nil || ((self.idToken?.isEmpty) == true) {
-
- completion(.failure(NSError(domain: "AppleProvider", code: 1, userInfo: [NSLocalizedDescriptionKey: "Not logged in; Cannot logout"])))
- return
- }
-
- self.idToken = nil
- self.refreshToken = nil
- self.accessToken = nil
-
- UserDefaults.standard.removeObject(forKey: SHARED_PREFERENCE_NAME)
+ // Sign-out is idempotent. Always erase transient tokens, the retained
+ // completion closure, and any identity/name state left by an earlier
+ // plugin build, even when this provider instance has no current token.
+ clearSessionState()
completion(.success(()))
- return
}
func refresh(completion: @escaping (Result<Void, Error>) -> Void) {
@@ -294,15 +245,11 @@
let fullName = appleIDCredential.fullName
let email = appleIDCredential.email
- // If we get a name, save it
- if fullName?.givenName != nil || fullName?.familyName != nil {
- persistName(userId: userIdentifier, givenName: fullName?.givenName, familyName: fullName?.familyName)
- }
-
- // Use saved name as fallback
- let savedName = retrieveName(userId: userIdentifier)
- let finalGivenName = fullName?.givenName ?? savedName?.givenName
- let finalFamilyName = fullName?.familyName ?? savedName?.familyName
+ // Apple sends a name only on first authorization. Pinstripe adopts
+ // it into the server profile immediately, so the provider must not
+ // retain a local user-id/name map after the sign-in attempt.
+ let finalGivenName = fullName?.givenName
+ let finalFamilyName = fullName?.familyName
// Create proper access token and decode JWT
let authorizationCode = String(data: appleIDCredential.authorizationCode ?? Data(), encoding: .utf8) ?? ""
@@ -334,7 +281,6 @@
if let decodedData = Data(base64Encoded: base64String, options: []),
let payload = try? JSONSerialization.jsonObject(with: decodedData, options: []) as? [String: Any] {
- print("payload", payload)
decodedEmail = payload["email"] as? String ?? email
}
}
@@ -401,7 +347,7 @@
// Convert the user dictionary to a JSON string
guard let userData = try? JSONSerialization.data(withJSONObject: user, options: []),
let userJSONString = String(data: userData, encoding: .utf8) else {
- print("Error converting user data to JSON string")
+ completion(.failure(.userDataSerializationError))
return
}
@@ -428,7 +374,6 @@
DispatchQueue.main.async {
if let error = error {
completion(.failure(.responseError(error)))
- print("Error: \(error)")
return
}
@@ -436,20 +381,14 @@
completion(.failure(.unknownError))
return
}
-
- print("Status Code: \(httpResponse.statusCode)")
// Check if the response is a redirect
if (300...399).contains(httpResponse.statusCode) {
if let location = httpResponse.value(forHTTPHeaderField: "Location") {
- print("Redirect Location: \(location)")
-
// Parse the redirect URL
if let redirectURL = URL(string: location),
let urlComponents = URLComponents(url: redirectURL, resolvingAgainstBaseURL: false),
let pathComponents = urlComponents.queryItems {
-
- print("Query items: \(String(describing: urlComponents.queryItems))")
// there are 4 main ways this can go:
// 1. it provides the "code" and we fetch apple servers in order to get the JWT (yuck)
@@ -532,23 +471,13 @@
} else {
completion(.failure(.pathComponentsNotFound))
- print("Path components not found")
return
}
} else {
completion(.failure(.noLocationHeader))
- print("No Location header found in the redirect response")
return
}
} else {
- // Handle non-redirect responses
- if let data = data,
- let responseString = String(data: data, encoding: .utf8) {
- print("Response: \(responseString)")
- } else {
- print("No response data received")
- }
-
completion(.failure(.invalidResponseCode(statusCode: httpResponse.statusCode)))
}
}
@@ -592,7 +521,6 @@
}
guard (200..<300).contains(httpResponse.statusCode) else {
- print("error", HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode))
completion(.failure(.invalidResponseCode(statusCode: httpResponse.statusCode)))
return
}
@@ -657,13 +585,6 @@
authorizationCode: nil
)
- // Log the tokens (replace with your logging mechanism)
- print("Apple Access Token is: \(accessToken)")
- print("Expires in: \(expiresIn)")
- print("Refresh token: \(refreshToken)")
- print("ID Token: \(idToken)")
- print("Apple User ID: \(userId)")
-
do {
try self.persistState(idToken: idToken, refreshToken: refreshToken, accessToken: accessToken)
} catch {
@@ -696,22 +617,4 @@
return windows.first { $0.isKeyWindow } ?? windows.first ?? UIApplication.shared.windows.first ?? ASPresentationAnchor()
}
- private func persistName(userId: String, givenName: String?, familyName: String?) {
- if givenName == nil && familyName == nil { return }
-
- var names = UserDefaults.standard.dictionary(forKey: USER_INFO_KEY) as? [String: [String: String]] ?? [:]
- names[userId] = [
- "givenName": givenName ?? "",
- "familyName": familyName ?? ""
- ]
- UserDefaults.standard.set(names, forKey: USER_INFO_KEY)
- }
-
- private func retrieveName(userId: String) -> (givenName: String?, familyName: String?)? {
- guard let names = UserDefaults.standard.dictionary(forKey: USER_INFO_KEY) as? [String: [String: String]],
- let userNames = names[userId] else {
- return nil
- }
- return (userNames["givenName"], userNames["familyName"])
- }
-}
+}
Sentry Apple SDK 9.33.0
The iOS app includes the Sentry Apple SDK for native crash and hang diagnostics, under the MIT License. The SDK source is available at the exact upstream release.
The MIT License (MIT) Copyright (c) 2015 Sentry Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Open-source notices describe third-party licensing and are not a replacement for the Terms of Service or Privacy Policy.