Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/LinkedIn #11

Merged
merged 8 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,20 @@
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "PovioKitAuthLinkedIn"
BuildableName = "PovioKitAuthLinkedIn"
BlueprintName = "PovioKitAuthLinkedIn"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
Expand Down
14 changes: 12 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import PackageDescription
let package = Package(
name: "PovioKitAuth",
platforms: [
.iOS(.v13)
.iOS(.v13),
.macOS(.v13)
],
products: [
.library(name: "PovioKitAuthCore", targets: ["PovioKitAuthCore"]),
.library(name: "PovioKitAuthApple", targets: ["PovioKitAuthApple"]),
.library(name: "PovioKitAuthGoogle", targets: ["PovioKitAuthGoogle"]),
.library(name: "PovioKitAuthFacebook", targets: ["PovioKitAuthFacebook"])
.library(name: "PovioKitAuthFacebook", targets: ["PovioKitAuthFacebook"]),
.library(name: "PovioKitAuthLinkedIn", targets: ["PovioKitAuthLinkedIn"])
],
dependencies: [
.package(url: "https://github.com/poviolabs/PovioKit", .upToNextMajor(from: "3.0.0")),
Expand Down Expand Up @@ -51,6 +53,14 @@ let package = Package(
],
path: "Sources/Facebook"
),
.target(
name: "PovioKitAuthLinkedIn",
dependencies: [
"PovioKitAuthCore",
.product(name: "PovioKitNetworking", package: "PovioKit")
],
path: "Sources/LinkedIn"
),
.testTarget(
name: "Tests",
dependencies: [
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@

## Packages

| [Core](Resources/Core) | [Apple](Resources/Apple) | [Google](Resources/Google) | [Facebook](Resources/Facebook) |
| :-: | :-: | :-: | :-: |
| [Core](Resources/Core) | [Apple](Resources/Apple) | [Google](Resources/Google) | [Facebook](Resources/Facebook) | [LinkedIn](Resources/LinkedIn) |
| :-: | :-: | :-: | :-: | :-: |

## Installation

Expand All @@ -38,6 +38,7 @@
- *PovioKitAuthApple* (Apple auth components)
- *PovioKitAuthGoogle* (Google auth components)
- *PovioKitAuthFacebook* (Facebook auth components)
- *PovioKitAuthLinkedIn* (LinkedIn auth components)
- Select "Add Package" again and you are done.

### Migration
Expand Down
32 changes: 32 additions & 0 deletions Resources/LinkedIn/README.md
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 modified Sources/.DS_Store
Binary file not shown.
23 changes: 23 additions & 0 deletions Sources/LinkedIn/API/EndpointEncodable.swift
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)
}
}
38 changes: 38 additions & 0 deletions Sources/LinkedIn/API/LinkedInAPI+Endpoints.swift
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)"
}
}
}
}
61 changes: 61 additions & 0 deletions Sources/LinkedIn/API/LinkedInAPI+Models.swift
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
}
}
79 changes: 79 additions & 0 deletions Sources/LinkedIn/API/LinkedInAPI.swift
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 struct LinkedInAPI {
borut-t marked this conversation as resolved.
Show resolved Hide resolved
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
}
}
47 changes: 47 additions & 0 deletions Sources/LinkedIn/LinkedInAuthenticator+Models.swift
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? {
borut-t marked this conversation as resolved.
Show resolved Hide resolved
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 {
tonikocjan marked this conversation as resolved.
Show resolved Hide resolved
public let userId: String
public let token: String
public let name: String
public let email: String
public let expiresAt: Date
}
}
Loading