-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
519 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# LinkedInAuthenticator | ||
|
||
Auth provider for social login with LinkedIn. | ||
|
||
## Setup | ||
Please read [official documentation](https://learn.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow?context=linkedin%2Fcontext&tabs=HTTPS1) from LinkedIn for the details about the authorization. | ||
|
||
## Usage | ||
|
||
```swift | ||
// present login screen | ||
body | ||
.sheet(isPresented: $openLinkedInWebView) { | ||
LinkedInWebView(with: linkedInConfig) { data in | ||
Task { await viewModel.signInWithLinkedIn(authCode: data.code) } | ||
} onFailure: { | ||
viewModel.error = .general | ||
} | ||
} | ||
|
||
// handle response from webView | ||
let authResponse = try await auth.signIn(authCode: authCode, configuration: linkedInConfig) | ||
|
||
// get authentication status | ||
let state = authenticator.isAuthenticated | ||
|
||
// signOut user | ||
authenticator.signOut() // all provider data regarding the use auth is cleared at this point | ||
|
||
// handle url | ||
authenticator.canOpenUrl(_: application: options:) // call this from `application:openURL:options:` in UIApplicationDelegate | ||
``` |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// | ||
// EndpointEncodable.swift | ||
// PovioKitAuth | ||
// | ||
// Created by Borut Tomazin on 04/09/2023. | ||
// Copyright © 2023 Povio Inc. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
import PovioKitNetworking | ||
|
||
protocol EndpointEncodable: URLConvertible { | ||
typealias Path = String | ||
|
||
var path: Path { get } | ||
var url: String { get } | ||
} | ||
|
||
extension EndpointEncodable { | ||
func asURL() throws -> URL { | ||
.init(stringLiteral: url) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
// | ||
// LinkedInAPI+Endpoints.swift | ||
// PovioKitAuth | ||
// | ||
// Created by Borut Tomazin on 04/09/2023. | ||
// Copyright © 2023 Povio Inc. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
import PovioKitNetworking | ||
|
||
extension LinkedInAPI { | ||
enum Endpoints: EndpointEncodable { | ||
case accessToken | ||
case profile | ||
case email | ||
|
||
var path: Path { | ||
switch self { | ||
case .accessToken: | ||
return "accessToken" | ||
case .profile: | ||
return "me" | ||
case .email: | ||
return "emailAddress?q=members&projection=(elements*(handle~))" | ||
} | ||
} | ||
|
||
var url: String { | ||
switch self { | ||
case .accessToken: | ||
return "https://www.linkedin.com/oauth/v2/\(path)" | ||
case .profile, .email: | ||
return "https://api.linkedin.com/v2/\(path)" | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
// | ||
// LinkedInAPI+Models.swift | ||
// PovioKitAuth | ||
// | ||
// Created by Borut Tomazin on 04/09/2023. | ||
// Copyright © 2023 Povio Inc. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
|
||
public extension LinkedInAPI { | ||
struct LinkedInAuthRequest: Encodable { | ||
let grantType: String = "authorization_code" | ||
let code: String | ||
let redirectUri: String | ||
let clientId: String | ||
let clientSecret: String | ||
|
||
public init(code: String, redirectUri: String, clientId: String, clientSecret: String) { | ||
self.code = code | ||
self.redirectUri = redirectUri | ||
self.clientId = clientId | ||
self.clientSecret = clientSecret | ||
} | ||
} | ||
|
||
struct LinkedInAuthResponse: Decodable { | ||
public let accessToken: String | ||
public let expiresIn: Date | ||
} | ||
|
||
struct LinkedInProfileRequest: Encodable { | ||
let token: String | ||
|
||
public init(token: String) { | ||
self.token = token | ||
} | ||
} | ||
|
||
struct LinkedInProfileResponse: Decodable { | ||
public let id: String | ||
public let localizedFirstName: String | ||
public let localizedLastName: String | ||
} | ||
|
||
struct LinkedInEmailResponse: Decodable { | ||
public let elements: [LinkedInEmailHandleResponse] | ||
} | ||
|
||
struct LinkedInEmailHandleResponse: Decodable { | ||
public let handle: LinkedInEmailValueResponse | ||
|
||
enum CodingKeys: String, CodingKey { | ||
case handle = "handle~" | ||
} | ||
} | ||
|
||
struct LinkedInEmailValueResponse: Decodable { | ||
public let emailAddress: String | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
// | ||
// LinkedInAPI.swift | ||
// PovioKitAuth | ||
// | ||
// Created by Borut Tomazin on 04/09/2023. | ||
// Copyright © 2023 Povio Inc. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
import PovioKitNetworking | ||
|
||
public final class LinkedInAPI { | ||
private let client: AlamofireNetworkClient | ||
|
||
public init(client: AlamofireNetworkClient = .init()) { | ||
self.client = client | ||
} | ||
} | ||
|
||
public extension LinkedInAPI { | ||
func login(with request: LinkedInAuthRequest) async throws -> LinkedInAuthResponse { | ||
let decoder = JSONDecoder() | ||
decoder.keyDecodingStrategy = .convertFromSnakeCase | ||
decoder.dateDecodingStrategy = .custom { decoder in | ||
let container = try decoder.singleValueContainer() | ||
let secondsRemaining = try container.decode(Int.self) | ||
return Date().addingTimeInterval(TimeInterval(secondsRemaining)) | ||
} | ||
|
||
let encoder = JSONEncoder() | ||
encoder.keyEncodingStrategy = .convertToSnakeCase | ||
|
||
return try await client | ||
.request( | ||
method: .post, | ||
endpoint: Endpoints.accessToken, | ||
encode: request, | ||
parameterEncoder: .urlEncoder(encoder: encoder) | ||
) | ||
.validate() | ||
.decode(LinkedInAuthResponse.self, decoder: decoder) | ||
.asAsync | ||
} | ||
|
||
func loadProfile(with request: LinkedInProfileRequest) async throws -> LinkedInProfileResponse { | ||
let decoder = JSONDecoder() | ||
decoder.keyDecodingStrategy = .convertFromSnakeCase | ||
decoder.dateDecodingStrategy = .iso8601 | ||
|
||
return try await client | ||
.request( | ||
method: .get, | ||
endpoint: Endpoints.profile, | ||
headers: ["Authorization": "Bearer \(request.token)"] | ||
) | ||
.validate() | ||
.decode(LinkedInProfileResponse.self, decoder: decoder) | ||
.asAsync | ||
} | ||
|
||
func loadEmail(with request: LinkedInProfileRequest) async throws -> LinkedInEmailValueResponse { | ||
return try await client | ||
.request( | ||
method: .get, | ||
endpoint: Endpoints.email, | ||
headers: ["Authorization": "Bearer \(request.token)"]) | ||
.validate() | ||
.decode(LinkedInEmailResponse.self) | ||
.compactMap { $0.elements.first?.handle } | ||
.asAsync | ||
} | ||
} | ||
|
||
// MARK: - Error | ||
public extension LinkedInAPI { | ||
enum Error: Swift.Error { | ||
case missingParameters | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
// | ||
// GoogleAuthenticator+Models.swift | ||
// PovioKitAuth | ||
// | ||
// Created by Borut Tomazin on 30/01/2023. | ||
// Copyright © 2023 Povio Inc. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
|
||
public extension LinkedInAuthenticator { | ||
struct Configuration { | ||
let clientId: String | ||
let clientSecret: String | ||
let permissions: String | ||
let redirectUrl: URL | ||
let authEndpoint: URL = "https://www.linkedin.com/oauth/v2/authorization" | ||
let authCancel: URL = "https://www.linkedin.com/oauth/v2/login-cancel" | ||
|
||
public init(clientId: String, clientSecret: String, permissions: String, redirectUrl: URL) { | ||
self.clientId = clientId | ||
self.clientSecret = clientSecret | ||
self.permissions = permissions | ||
self.redirectUrl = redirectUrl | ||
} | ||
|
||
func authorizationUrl(state: String) -> URL? { | ||
guard var urlComponents = URLComponents(url: authEndpoint, resolvingAgainstBaseURL: false) else { return nil } | ||
urlComponents.queryItems = [ | ||
.init(name: "response_type", value: "code"), | ||
.init(name: "client_id", value: clientId), | ||
.init(name: "redirect_uri", value: redirectUrl.absoluteString), | ||
.init(name: "state", value: state), | ||
.init(name: "scope", value: permissions) | ||
] | ||
return urlComponents.url | ||
} | ||
} | ||
|
||
struct Response { | ||
public let userId: String | ||
public let token: String | ||
public let name: String | ||
public let email: String | ||
public let expiresAt: Date | ||
} | ||
} |
Oops, something went wrong.