From 0f335a2dda4485ed5f47c9bdb1cc23d6551c4c30 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Mon, 10 Apr 2023 06:10:34 -0300 Subject: [PATCH 1/6] Add sign in with id token method --- Sources/GoTrue/GoTrueClient.swift | 26 +++++++++++----- Sources/GoTrue/Types.swift | 50 ++++++++++++++++++------------- 2 files changed, 48 insertions(+), 28 deletions(-) diff --git a/Sources/GoTrue/GoTrueClient.swift b/Sources/GoTrue/GoTrueClient.swift index 04aeedc..a1df706 100644 --- a/Sources/GoTrue/GoTrueClient.swift +++ b/Sources/GoTrue/GoTrueClient.swift @@ -97,7 +97,7 @@ public final class GoTrueClient { email: email, password: password, data: data, - gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init(hcaptchaToken:)) + gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init) ) ) ) @@ -121,7 +121,7 @@ public final class GoTrueClient { password: password, phone: phone, data: data, - gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init(hcaptchaToken:)) + gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init) ) ) ) @@ -161,6 +161,18 @@ public final class GoTrueClient { ) } + /// Allows signing in with an ID token issued by certain supported providers. + /// The ID token is verified for validity and a new session is established. + @discardableResult + public func signInWithIdToken(credentials: OpenIDConnectCredentials) async throws -> Session { + try await _signIn( + request: Paths.token.post( + grantType: .idToken, + .openIDConnectCredentials(credentials) + ) + ) + } + private func _signIn(request: Request) async throws -> Session { await Env.sessionManager.remove() @@ -200,7 +212,7 @@ public final class GoTrueClient { email: email, createUser: shouldCreateUser, data: data, - gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init(hcaptchaToken:)) + gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init) ) ) ) @@ -226,7 +238,7 @@ public final class GoTrueClient { phone: phone, createUser: shouldCreateUser, data: data, - gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init(hcaptchaToken:)) + gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init) ) ) ) @@ -420,7 +432,7 @@ public final class GoTrueClient { email: email, token: token, type: type, - gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init(hcaptchaToken:)) + gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init) ) ) ) @@ -440,7 +452,7 @@ public final class GoTrueClient { phone: phone, token: token, type: type, - gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init(hcaptchaToken:)) + gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init) ) ) ) @@ -483,7 +495,7 @@ public final class GoTrueClient { redirectTo: redirectTo, RecoverParams( email: email, - gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init(hcaptchaToken:)) + gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init) ) ) ).value diff --git a/Sources/GoTrue/Types.swift b/Sources/GoTrue/Types.swift index 932e64b..f1c52ff 100644 --- a/Sources/GoTrue/Types.swift +++ b/Sources/GoTrue/Types.swift @@ -301,44 +301,52 @@ public enum Provider: String, Codable, CaseIterable { } public struct OpenIDConnectCredentials: Codable, Hashable { - public var idToken: String - public var nonce: String - public var clientID: String? - public var issuer: String? + /// Only Apple and Google ID tokens are supported for use from within iOS or Android applications. public var provider: Provider? + /// ID token issued by Apple or Google. + public var token: String + + /// If the ID token contains a `nonce`, then the hash of this value is compared to the value in + /// the ID token. + public var nonce: String? + + /// Verification token received when the user completes the captcha on the site. + public var gotrueMetaSecurity: GoTrueMetaSecurity? + public init( - idToken: String, - nonce: String, - clientID: String? = nil, - issuer: String? = nil, - provider: Provider? = nil + provider: Provider? = nil, + token: String, + nonce: String? = nil, + gotrueMetaSecurity: GoTrueMetaSecurity? = nil ) { - self.idToken = idToken - self.nonce = nonce - self.clientID = clientID - self.issuer = issuer self.provider = provider + self.token = token + self.nonce = nonce + self.gotrueMetaSecurity = gotrueMetaSecurity } public enum CodingKeys: String, CodingKey { - case idToken = "id_token" - case nonce - case clientID = "client_id" - case issuer case provider + case token + case nonce + case gotrueMetaSecurity = "gotrue_meta_security" + } + + public enum Provider: String, Codable, Hashable { + case google, apple } } public struct GoTrueMetaSecurity: Codable, Hashable { - public var hcaptchaToken: String + public var captchaToken: String - public init(hcaptchaToken: String) { - self.hcaptchaToken = hcaptchaToken + public init(captchaToken: String) { + self.captchaToken = captchaToken } public enum CodingKeys: String, CodingKey { - case hcaptchaToken = "hcaptcha_token" + case captchaToken = "captcha_token" } } From 7841fed4f91adc86628236603d6fad52edabf074 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Mon, 10 Apr 2023 06:30:30 -0300 Subject: [PATCH 2/6] Add test --- Tests/GoTrueTests/GoTrueTests.swift | 55 ++++++------------------ Tests/GoTrueTests/Resources/session.json | 37 ++++++++++++++++ 2 files changed, 51 insertions(+), 41 deletions(-) create mode 100644 Tests/GoTrueTests/Resources/session.json diff --git a/Tests/GoTrueTests/GoTrueTests.swift b/Tests/GoTrueTests/GoTrueTests.swift index ebf7c54..2ac24a9 100644 --- a/Tests/GoTrueTests/GoTrueTests.swift +++ b/Tests/GoTrueTests/GoTrueTests.swift @@ -40,7 +40,7 @@ final class GoTrueTests: XCTestCase { func testDecodeSessionOrUser() { XCTAssertNoThrow( try JSONDecoder.goTrue.decode( - AuthResponse.self, from: sessionJSON.data(using: .utf8)! + AuthResponse.self, from: json(named: "session") ) ) } @@ -56,6 +56,19 @@ final class GoTrueTests: XCTestCase { } #endif + func testSignInWithIdToken() async throws { + Mock( + url: URL(string: "http://localhost:54321/auth/v1/token?grant_type=id_token")!, + dataType: .json, + statusCode: 200, + data: [.post: json(named: "session")] + ).register() + + let session = try await sut + .signInWithIdToken(credentials: OpenIDConnectCredentials(token: "dummy-token-1234")) + XCTAssertEqual(session.user.email, "guilherme@binaryscraping.co") + } + func testSignInWithProvider() throws { let url = try sut.getOAuthSignInURL( provider: .github, scopes: "read,write", @@ -110,43 +123,3 @@ final class GoTrueTests: XCTestCase { } } } - -let sessionJSON = """ -{ -"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjQ4NjQwMDIxLCJzdWIiOiJmMzNkM2VjOS1hMmVlLTQ3YzQtODBlMS01YmQ5MTlmM2Q4YjgiLCJlbWFpbCI6Imd1aWxoZXJtZTJAZ3Jkcy5kZXYiLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7fSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQifQ.4lMvmz2pJkWu1hMsBgXP98Fwz4rbvFYl4VA9joRv6kY", -"token_type": "bearer", -"expires_in": 3600, -"refresh_token": "GGduTeu95GraIXQ56jppkw", -"user": { -"id": "f33d3ec9-a2ee-47c4-80e1-5bd919f3d8b8", -"aud": "authenticated", -"role": "authenticated", -"email": "guilherme2@grds.dev", -"email_confirmed_at": "2022-03-30T10:33:41.018575157Z", -"phone": "", -"last_sign_in_at": "2022-03-30T10:33:41.021531328Z", -"app_metadata": { -"provider": "email", -"providers": [ -"email" -] -}, -"user_metadata": {}, -"identities": [ -{ -"id": "f33d3ec9-a2ee-47c4-80e1-5bd919f3d8b8", -"user_id": "f33d3ec9-a2ee-47c4-80e1-5bd919f3d8b8", -"identity_data": { - "sub": "f33d3ec9-a2ee-47c4-80e1-5bd919f3d8b8" -}, -"provider": "email", -"last_sign_in_at": "2022-03-30T10:33:41.015557063Z", -"created_at": "2022-03-30T10:33:41.015612Z", -"updated_at": "2022-03-30T10:33:41.015616Z" -} -], -"created_at": "2022-03-30T10:33:41.005433Z", -"updated_at": "2022-03-30T10:33:41.022688Z" -} -} -""" diff --git a/Tests/GoTrueTests/Resources/session.json b/Tests/GoTrueTests/Resources/session.json new file mode 100644 index 0000000..24eeff1 --- /dev/null +++ b/Tests/GoTrueTests/Resources/session.json @@ -0,0 +1,37 @@ +{ + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjQ4NjQwMDIxLCJzdWIiOiJmMzNkM2VjOS1hMmVlLTQ3YzQtODBlMS01YmQ5MTlmM2Q4YjgiLCJlbWFpbCI6Imd1aWxoZXJtZTJAZ3Jkcy5kZXYiLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7fSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQifQ.4lMvmz2pJkWu1hMsBgXP98Fwz4rbvFYl4VA9joRv6kY", + "token_type": "bearer", + "expires_in": 3600, + "refresh_token": "GGduTeu95GraIXQ56jppkw", + "user": { + "id": "f33d3ec9-a2ee-47c4-80e1-5bd919f3d8b8", + "aud": "authenticated", + "role": "authenticated", + "email": "guilherme@binaryscraping.co", + "email_confirmed_at": "2022-03-30T10:33:41.018575157Z", + "phone": "", + "last_sign_in_at": "2022-03-30T10:33:41.021531328Z", + "app_metadata": { + "provider": "email", + "providers": [ + "email" + ] + }, + "user_metadata": {}, + "identities": [ + { + "id": "f33d3ec9-a2ee-47c4-80e1-5bd919f3d8b8", + "user_id": "f33d3ec9-a2ee-47c4-80e1-5bd919f3d8b8", + "identity_data": { + "sub": "f33d3ec9-a2ee-47c4-80e1-5bd919f3d8b8" + }, + "provider": "email", + "last_sign_in_at": "2022-03-30T10:33:41.015557063Z", + "created_at": "2022-03-30T10:33:41.015612Z", + "updated_at": "2022-03-30T10:33:41.015616Z" + } + ], + "created_at": "2022-03-30T10:33:41.005433Z", + "updated_at": "2022-03-30T10:33:41.022688Z" + } +} From bc737ae36c1b999af2e99a17c01f2ea8e4bcb325 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Mon, 10 Apr 2023 06:32:46 -0300 Subject: [PATCH 3/6] ci: copy secrets file --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43302fa..c566a7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,8 @@ jobs: - uses: actions/checkout@v3 - name: Select Xcode ${{ matrix.xcode }} run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app + - name: Copy Secrets file + run: cp Examples/Shared/Sources/_Secrets.swift Examples/Shared/Sources/Secrets.swift - name: Build example run: make build-example From b4f85a67b53735363937f516acee716ef305cf38 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Mon, 10 Apr 2023 07:46:59 -0300 Subject: [PATCH 4/6] Format and fix tests --- .swift-version | 1 - .swiftformat | 1 + Examples/Shared/Sources/AppView.swift | 2 +- Sources/GoTrue/GoTrueClient.swift | 8 +++--- Sources/GoTrue/GoTrueLocalStorage.swift | 2 +- Sources/GoTrue/Internal/SessionManager.swift | 2 +- Tests/GoTrueTests/GoTrueTests.swift | 27 ++++++++++---------- 7 files changed, 21 insertions(+), 22 deletions(-) delete mode 100644 .swift-version diff --git a/.swift-version b/.swift-version deleted file mode 100644 index 2df33d7..0000000 --- a/.swift-version +++ /dev/null @@ -1 +0,0 @@ -5.6 diff --git a/.swiftformat b/.swiftformat index ae46a8c..fc594a2 100644 --- a/.swiftformat +++ b/.swiftformat @@ -1,3 +1,4 @@ +--swiftversion 5.7 --binarygrouping none --decimalgrouping none --hexgrouping none diff --git a/Examples/Shared/Sources/AppView.swift b/Examples/Shared/Sources/AppView.swift index 464325f..a0cf5db 100644 --- a/Examples/Shared/Sources/AppView.swift +++ b/Examples/Shared/Sources/AppView.swift @@ -38,7 +38,7 @@ struct AppView: View { } } -func stringfy(_ value: T) -> String { +func stringfy(_ value: some Codable) -> String { let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys] let data = try? encoder.encode(value) diff --git a/Sources/GoTrue/GoTrueClient.swift b/Sources/GoTrue/GoTrueClient.swift index a1df706..3e560e8 100644 --- a/Sources/GoTrue/GoTrueClient.swift +++ b/Sources/GoTrue/GoTrueClient.swift @@ -263,11 +263,11 @@ public final class GoTrueClient { URLQueryItem(name: "provider", value: provider.rawValue), ] - if let scopes = scopes { + if let scopes { queryItems.append(URLQueryItem(name: "scopes", value: scopes)) } - if let redirectTo = redirectTo { + if let redirectTo { queryItems.append(URLQueryItem(name: "redirect_to", value: redirectTo.absoluteString)) } @@ -395,7 +395,7 @@ public final class GoTrueClient { ) } - guard let session = session else { + guard let session else { throw GoTrueError.sessionNotFound } @@ -411,7 +411,7 @@ public final class GoTrueClient { let session = try? await Env.sessionManager.session() await Env.sessionManager.remove() - if let session = session { + if let session { try await Env.client.send(Paths.logout.post.withAuthorization(session.accessToken)).value } } diff --git a/Sources/GoTrue/GoTrueLocalStorage.swift b/Sources/GoTrue/GoTrueLocalStorage.swift index e18eba2..693e9ac 100644 --- a/Sources/GoTrue/GoTrueLocalStorage.swift +++ b/Sources/GoTrue/GoTrueLocalStorage.swift @@ -11,7 +11,7 @@ struct KeychainLocalStorage: GoTrueLocalStorage { let keychain: Keychain init(service: String, accessGroup: String?) { - if let accessGroup = accessGroup { + if let accessGroup { keychain = Keychain(service: service, accessGroup: accessGroup) } else { keychain = Keychain(service: service) diff --git a/Sources/GoTrue/Internal/SessionManager.swift b/Sources/GoTrue/Internal/SessionManager.swift index 0074de2..86e2754 100644 --- a/Sources/GoTrue/Internal/SessionManager.swift +++ b/Sources/GoTrue/Internal/SessionManager.swift @@ -36,7 +36,7 @@ private actor LiveSessionManager { private var task: Task? func session() async throws -> Session { - if let task = task { + if let task { return try await task.value } diff --git a/Tests/GoTrueTests/GoTrueTests.swift b/Tests/GoTrueTests/GoTrueTests.swift index 2ac24a9..938f562 100644 --- a/Tests/GoTrueTests/GoTrueTests.swift +++ b/Tests/GoTrueTests/GoTrueTests.swift @@ -46,7 +46,6 @@ final class GoTrueTests: XCTestCase { } #if !os(watchOS) - // Not working on watchOS, don't know why. func test_signUpWithEmailAndPassword() async throws { Mock.post(path: "signup", json: "signup-response").register() @@ -54,20 +53,20 @@ final class GoTrueTests: XCTestCase { XCTAssertEqual(user?.email, "guilherme@grds.dev") } - #endif - func testSignInWithIdToken() async throws { - Mock( - url: URL(string: "http://localhost:54321/auth/v1/token?grant_type=id_token")!, - dataType: .json, - statusCode: 200, - data: [.post: json(named: "session")] - ).register() - - let session = try await sut - .signInWithIdToken(credentials: OpenIDConnectCredentials(token: "dummy-token-1234")) - XCTAssertEqual(session.user.email, "guilherme@binaryscraping.co") - } + func testSignInWithIdToken() async throws { + Mock( + url: URL(string: "http://localhost:54321/auth/v1/token?grant_type=id_token")!, + dataType: .json, + statusCode: 200, + data: [.post: json(named: "session")] + ).register() + + let session = try await sut + .signInWithIdToken(credentials: OpenIDConnectCredentials(token: "dummy-token-1234")) + XCTAssertEqual(session.user.email, "guilherme@binaryscraping.co") + } + #endif func testSignInWithProvider() throws { let url = try sut.getOAuthSignInURL( From b8c2a374fe86ead08e24fbb5e7045473f6630161 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Mon, 10 Apr 2023 07:50:43 -0300 Subject: [PATCH 5/6] Add deprecations --- Sources/GoTrue/Deprecated.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 Sources/GoTrue/Deprecated.swift diff --git a/Sources/GoTrue/Deprecated.swift b/Sources/GoTrue/Deprecated.swift new file mode 100644 index 0000000..3db80c6 --- /dev/null +++ b/Sources/GoTrue/Deprecated.swift @@ -0,0 +1,14 @@ +import Foundation + +extension GoTrueMetaSecurity { + @available(*, deprecated, renamed: "captchaToken") + public var hcaptchaToken: String { + get { captchaToken } + set { captchaToken = newValue } + } + + @available(*, deprecated, renamed: "init(captchaToken:)") + public init(hcaptchaToken: String) { + self.init(captchaToken: hcaptchaToken) + } +} From a20217a48d71ef00c92efcb44337999c4890ebf8 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Mon, 10 Apr 2023 08:00:05 -0300 Subject: [PATCH 6/6] Fix initializer --- Sources/GoTrue/GoTrueClient.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/GoTrue/GoTrueClient.swift b/Sources/GoTrue/GoTrueClient.swift index 3e560e8..e27b8b9 100644 --- a/Sources/GoTrue/GoTrueClient.swift +++ b/Sources/GoTrue/GoTrueClient.swift @@ -97,7 +97,7 @@ public final class GoTrueClient { email: email, password: password, data: data, - gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init) + gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init(captchaToken:)) ) ) ) @@ -121,7 +121,7 @@ public final class GoTrueClient { password: password, phone: phone, data: data, - gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init) + gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init(captchaToken:)) ) ) ) @@ -212,7 +212,7 @@ public final class GoTrueClient { email: email, createUser: shouldCreateUser, data: data, - gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init) + gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init(captchaToken:)) ) ) ) @@ -238,7 +238,7 @@ public final class GoTrueClient { phone: phone, createUser: shouldCreateUser, data: data, - gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init) + gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init(captchaToken:)) ) ) ) @@ -432,7 +432,7 @@ public final class GoTrueClient { email: email, token: token, type: type, - gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init) + gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init(captchaToken:)) ) ) ) @@ -452,7 +452,7 @@ public final class GoTrueClient { phone: phone, token: token, type: type, - gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init) + gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init(captchaToken:)) ) ) ) @@ -495,7 +495,7 @@ public final class GoTrueClient { redirectTo: redirectTo, RecoverParams( email: email, - gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init) + gotrueMetaSecurity: captchaToken.map(GoTrueMetaSecurity.init(captchaToken:)) ) ) ).value