Skip to content

Commit

Permalink
Feature/LinkedIn (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
borut-t authored Sep 28, 2023
1 parent c584c0c commit e2f70df
Show file tree
Hide file tree
Showing 13 changed files with 519 additions and 4 deletions.
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 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
}
}
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? {
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
}
}
Loading

0 comments on commit e2f70df

Please sign in to comment.