diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/VaporDeviceCheck.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/VaporDeviceCheck.xcscheme
new file mode 100644
index 0000000..c26c341
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/VaporDeviceCheck.xcscheme
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Package.resolved b/Package.resolved
index 5ce1f3f..e2d0cd7 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -1,151 +1,275 @@
{
- "object": {
- "pins": [
- {
- "package": "async-http-client",
- "repositoryURL": "https://github.com/swift-server/async-http-client.git",
- "state": {
- "branch": null,
- "revision": "037b70291941fe43de668066eb6fb802c5e181d2",
- "version": "1.1.1"
- }
- },
- {
- "package": "async-kit",
- "repositoryURL": "https://github.com/vapor/async-kit.git",
- "state": {
- "branch": null,
- "revision": "7457413e57dbfac762b32dd30c1caf2c55a02a3d",
- "version": "1.2.0"
- }
- },
- {
- "package": "console-kit",
- "repositoryURL": "https://github.com/vapor/console-kit.git",
- "state": {
- "branch": null,
- "revision": "9b5842b47be1a3164a42811613dce09bf5bf1f91",
- "version": "4.1.3"
- }
- },
- {
- "package": "jwt",
- "repositoryURL": "https://github.com/vapor/jwt.git",
- "state": {
- "branch": null,
- "revision": "245dc755c2202a21c4108de0ecfd05aab174e8ab",
- "version": "4.0.0-rc.2.1"
- }
- },
- {
- "package": "jwt-kit",
- "repositoryURL": "https://github.com/vapor/jwt-kit.git",
- "state": {
- "branch": null,
- "revision": "b5b711fd6ee5638552a29a0c39003aeb109caa94",
- "version": "4.0.0-rc.1.5"
- }
- },
- {
- "package": "routing-kit",
- "repositoryURL": "https://github.com/vapor/routing-kit.git",
- "state": {
- "branch": null,
- "revision": "e7f2d5bd36dc65a9edb303541cb678515a7fece3",
- "version": "4.1.0"
- }
- },
- {
- "package": "swift-backtrace",
- "repositoryURL": "https://github.com/swift-server/swift-backtrace.git",
- "state": {
- "branch": null,
- "revision": "f2fd8c4845a123419c348e0bc4b3839c414077d5",
- "version": "1.2.0"
- }
- },
- {
- "package": "swift-crypto",
- "repositoryURL": "https://github.com/apple/swift-crypto.git",
- "state": {
- "branch": null,
- "revision": "9b9d1868601a199334da5d14f4ab2d37d4f8d0c5",
- "version": "1.0.2"
- }
- },
- {
- "package": "swift-log",
- "repositoryURL": "https://github.com/apple/swift-log.git",
- "state": {
- "branch": null,
- "revision": "57c6bd04256ba47590ee2285e208f731210c5c10",
- "version": "1.3.0"
- }
- },
- {
- "package": "swift-metrics",
- "repositoryURL": "https://github.com/apple/swift-metrics.git",
- "state": {
- "branch": null,
- "revision": "708b960b4605abb20bc55d65abf6bad607252200",
- "version": "2.0.0"
- }
- },
- {
- "package": "swift-nio",
- "repositoryURL": "https://github.com/apple/swift-nio.git",
- "state": {
- "branch": null,
- "revision": "8a865bd15e69526cbdfcfd7c47698eb20b2ba951",
- "version": "2.19.0"
- }
- },
- {
- "package": "swift-nio-extras",
- "repositoryURL": "https://github.com/apple/swift-nio-extras.git",
- "state": {
- "branch": null,
- "revision": "7cd24c0efcf9700033f671b6a8eaa64a77dd0b72",
- "version": "1.5.1"
- }
- },
- {
- "package": "swift-nio-http2",
- "repositoryURL": "https://github.com/apple/swift-nio-http2.git",
- "state": {
- "branch": null,
- "revision": "c76a9a5085bfc22882f8cff88189662af30806e8",
- "version": "1.12.3"
- }
- },
- {
- "package": "swift-nio-ssl",
- "repositoryURL": "https://github.com/apple/swift-nio-ssl.git",
- "state": {
- "branch": null,
- "revision": "d381bc53edd9de88a75480a2b969bfc26d61ee76",
- "version": "2.8.0"
- }
- },
- {
- "package": "vapor",
- "repositoryURL": "https://github.com/vapor/vapor.git",
- "state": {
- "branch": null,
- "revision": "dc2aa1e02e04a47b67cb0dabed628fe844900f30",
- "version": "4.14.0"
- }
- },
- {
- "package": "websocket-kit",
- "repositoryURL": "https://github.com/vapor/websocket-kit.git",
- "state": {
- "branch": null,
- "revision": "021edd1ca55451ad15b3e84da6b4064e4b877b34",
- "version": "2.1.0"
- }
- }
- ]
- },
- "version": 1
+ "pins" : [
+ {
+ "identity" : "async-http-client",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/swift-server/async-http-client.git",
+ "state" : {
+ "revision" : "60235983163d040f343a489f7e2e77c1918a8bd9",
+ "version" : "1.26.1"
+ }
+ },
+ {
+ "identity" : "async-kit",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/vapor/async-kit.git",
+ "state" : {
+ "revision" : "e048c8ee94967e8d8a1c2ec0e1156d6f7fa34d31",
+ "version" : "1.20.0"
+ }
+ },
+ {
+ "identity" : "console-kit",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/vapor/console-kit.git",
+ "state" : {
+ "revision" : "742f624a998cba2a9e653d9b1e91ad3f3a5dff6b",
+ "version" : "4.15.2"
+ }
+ },
+ {
+ "identity" : "jwt",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/vapor/jwt.git",
+ "state" : {
+ "revision" : "af1c59762d70d1065ddbc0d7902ea9b3dacd1a26",
+ "version" : "5.1.2"
+ }
+ },
+ {
+ "identity" : "jwt-kit",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/vapor/jwt-kit.git",
+ "state" : {
+ "revision" : "2033b3e661238dda3d30e36a2d40987499d987de",
+ "version" : "5.2.0"
+ }
+ },
+ {
+ "identity" : "multipart-kit",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/vapor/multipart-kit.git",
+ "state" : {
+ "revision" : "3498e60218e6003894ff95192d756e238c01f44e",
+ "version" : "4.7.1"
+ }
+ },
+ {
+ "identity" : "routing-kit",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/vapor/routing-kit.git",
+ "state" : {
+ "revision" : "93f7222c8e195cbad39fafb5a0e4cc85a8def7ea",
+ "version" : "4.9.2"
+ }
+ },
+ {
+ "identity" : "swift-algorithms",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-algorithms.git",
+ "state" : {
+ "revision" : "87e50f483c54e6efd60e885f7f5aa946cee68023",
+ "version" : "1.2.1"
+ }
+ },
+ {
+ "identity" : "swift-asn1",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-asn1.git",
+ "state" : {
+ "revision" : "f70225981241859eb4aa1a18a75531d26637c8cc",
+ "version" : "1.4.0"
+ }
+ },
+ {
+ "identity" : "swift-async-algorithms",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-async-algorithms.git",
+ "state" : {
+ "revision" : "042e1c4d9d19748c9c228f8d4ebc97bb1e339b0b",
+ "version" : "1.0.4"
+ }
+ },
+ {
+ "identity" : "swift-atomics",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-atomics.git",
+ "state" : {
+ "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7",
+ "version" : "1.3.0"
+ }
+ },
+ {
+ "identity" : "swift-certificates",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-certificates.git",
+ "state" : {
+ "revision" : "870f4d5fe5fcfedc13f25d70e103150511746404",
+ "version" : "1.11.0"
+ }
+ },
+ {
+ "identity" : "swift-collections",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-collections.git",
+ "state" : {
+ "revision" : "8c0c0a8b49e080e54e5e328cc552821ff07cd341",
+ "version" : "1.2.1"
+ }
+ },
+ {
+ "identity" : "swift-crypto",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-crypto.git",
+ "state" : {
+ "revision" : "84b1d494118d63629a785230135f82991f02329e",
+ "version" : "3.13.2"
+ }
+ },
+ {
+ "identity" : "swift-distributed-tracing",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-distributed-tracing.git",
+ "state" : {
+ "revision" : "b78796709d243d5438b36e74ce3c5ec2d2ece4d8",
+ "version" : "1.2.1"
+ }
+ },
+ {
+ "identity" : "swift-http-structured-headers",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-http-structured-headers.git",
+ "state" : {
+ "revision" : "db6eea3692638a65e2124990155cd220c2915903",
+ "version" : "1.3.0"
+ }
+ },
+ {
+ "identity" : "swift-http-types",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-http-types.git",
+ "state" : {
+ "revision" : "a0a57e949a8903563aba4615869310c0ebf14c03",
+ "version" : "1.4.0"
+ }
+ },
+ {
+ "identity" : "swift-log",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-log.git",
+ "state" : {
+ "revision" : "ce592ae52f982c847a4efc0dd881cc9eb32d29f2",
+ "version" : "1.6.4"
+ }
+ },
+ {
+ "identity" : "swift-metrics",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-metrics.git",
+ "state" : {
+ "revision" : "4c83e1cdf4ba538ef6e43a9bbd0bcc33a0ca46e3",
+ "version" : "2.7.0"
+ }
+ },
+ {
+ "identity" : "swift-nio",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-nio.git",
+ "state" : {
+ "revision" : "a5fea865badcb1c993c85b0f0e8d05a4bd2270fb",
+ "version" : "2.85.0"
+ }
+ },
+ {
+ "identity" : "swift-nio-extras",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-nio-extras.git",
+ "state" : {
+ "revision" : "a55c3dd3a81d035af8a20ce5718889c0dcab073d",
+ "version" : "1.29.0"
+ }
+ },
+ {
+ "identity" : "swift-nio-http2",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-nio-http2.git",
+ "state" : {
+ "revision" : "5e9e99ec96c53bc2c18ddd10c1e25a3cd97c55e5",
+ "version" : "1.38.0"
+ }
+ },
+ {
+ "identity" : "swift-nio-ssl",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-nio-ssl.git",
+ "state" : {
+ "revision" : "385f5bd783ffbfff46b246a7db7be8e4f04c53bd",
+ "version" : "2.33.0"
+ }
+ },
+ {
+ "identity" : "swift-nio-transport-services",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-nio-transport-services.git",
+ "state" : {
+ "revision" : "decfd235996bc163b44e10b8a24997a3d2104b90",
+ "version" : "1.25.0"
+ }
+ },
+ {
+ "identity" : "swift-numerics",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-numerics.git",
+ "state" : {
+ "revision" : "e0ec0f5f3af6f3e4d5e7a19d2af26b481acb6ba8",
+ "version" : "1.0.3"
+ }
+ },
+ {
+ "identity" : "swift-service-context",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-service-context.git",
+ "state" : {
+ "revision" : "1983448fefc717a2bc2ebde5490fe99873c5b8a6",
+ "version" : "1.2.1"
+ }
+ },
+ {
+ "identity" : "swift-service-lifecycle",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/swift-server/swift-service-lifecycle.git",
+ "state" : {
+ "revision" : "e7187309187695115033536e8fc9b2eb87fd956d",
+ "version" : "2.8.0"
+ }
+ },
+ {
+ "identity" : "swift-system",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-system.git",
+ "state" : {
+ "revision" : "41daa93a5d229e1548ec86ab527ce4783ca84dda",
+ "version" : "1.6.0"
+ }
+ },
+ {
+ "identity" : "vapor",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/vapor/vapor.git",
+ "state" : {
+ "revision" : "3636f443474769147828a5863e81a31f6f30e92c",
+ "version" : "4.115.1"
+ }
+ },
+ {
+ "identity" : "websocket-kit",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/vapor/websocket-kit.git",
+ "state" : {
+ "revision" : "8666c92dbbb3c8eefc8008c9c8dcf50bfd302167",
+ "version" : "2.16.1"
+ }
+ }
+ ],
+ "version" : 2
}
diff --git a/Package.swift b/Package.swift
index c53159f..19d6601 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,11 +1,11 @@
-// swift-tools-version:5.2
+// swift-tools-version:5.8
import PackageDescription
let package = Package(
name: "VaporDeviceCheck",
platforms: [
- .macOS(.v10_15)
+ .macOS(.v13)
],
products: [
.library(
@@ -14,7 +14,7 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "4.14.0"),
- .package(url: "https://github.com/vapor/jwt.git", from: "4.0.0-rc.2.1")
+ .package(url: "https://github.com/vapor/jwt.git", from: "5.0.0")
],
targets: [
.target(
diff --git a/README.md b/README.md
index a93ccf1..c0ae810 100644
--- a/README.md
+++ b/README.md
@@ -10,9 +10,11 @@ First add the package to your `Package.swift`:
.package(url: "https://github.com/Bearologics/VaporDeviceCheck", from: "1.0.1")
```
-Then configure your Vapor `Application` and make sure to set up the JWT credentials to authenticate against the DeviceCheck API, in this example we're using environment variables which are prefixed `APPLE_JWT_` and install the Middleware:
+Then configure your Vapor `Application` and make sure to set up the JWT credentials to authenticate against the DeviceCheck API, in this example we're using environment variables which are prefixed `APPLE_JWT_` and install the Middleware with this setup function:
```swift
+import Vapor
+import JWTKit
import VaporDeviceCheck
enum ConfigurationError: Error {
@@ -20,37 +22,57 @@ enum ConfigurationError: Error {
}
// configures your application
-public func configure(_ app: Application) throws {
- guard let jwtPrivateKeyString = Environment.get("APPLE_JWT_PRIVATE_KEY") else {
+public func configureDeviceCheck(_ app: Application) async throws {
+ guard let jwtPrivateKeyStringEscaped = Environment.get("APPLE_JWT_PRIVATE_KEY") else {
throw ConfigurationError.noAppleJwtPrivateKey
}
-
+ let jwtPrivateKeyString = jwtPrivateKeyStringEscaped.replacingOccurrences(of: "\\n", with: "\n")
+
guard let jwtKidString = Environment.get("APPLE_JWT_KID") else {
throw ConfigurationError.noAppleJwtKid
}
-
- guard let jwkIssString = Environment.get("APPLE_JWT_ISS") else {
+ guard let jwtIss = Environment.get("APPLE_JWT_ISS") else {
throw ConfigurationError.noAppleJwtIss
}
-
- let jwkKid = JWKIdentifier(string: jwtKidString)
-
- app.jwt.signers.use(
- .es256(key: try! .private(pem: jwtPrivateKeyString.data(using: .utf8)!)),
- kid: jwkKid,
- isDefault: false
- )
-
- // install middleware
- app.middleware.use(DeviceCheck(jwkKid: jwkKid, jwkIss: jwkIssString, excludes: [["health"]]))
-
- // register routes
- try routes(app)
+
+ let jwtBypassToken = Environment.get("APPLE_JWT_BYPASS_TOKEN")
+
+ let kid = JWKIdentifier(string: jwtKidString)
+ let privateKey = try ES256PrivateKey(pem: Data(jwtPrivateKeyString.utf8))
+
+ // Add ECDSA key with JWKIdentifier
+ await app.jwt.keys.add(ecdsa: privateKey, kid: kid)
+
+ app.middleware.use(DeviceCheck(
+ jwkKid: kid,
+ jwkIss: jwtIss,
+ excludes: [["health"]],
+ bypassTokens: jwtBypassToken == nil ? [] : [jwtBypassToken!]
+ ))
+}
+```
+
+Then you call it from configure:
+
+```swift
+public func configure(_ app: Application) async throws {
+ try await configureDeviceCheck(app)
+ ...
}
```
That's basically it, from now on, every request that'll pass the Middleware will require a valid `X-Apple-Device-Token` header to be set, otherwise it will be rejected.
+> **Note:** You can pass in the private key either multilined or single-lined separated by `\n` and it will parse the key correctly.
+
+## Environment Variable Breakdown
+| Environment Key | Description |
+| ---------------------- | ----------------------------------------------------------------------------------------------------------- |
+| APPLE_JWT_PRIVATE_KEY | The full private key you downloaded as a string. |
+| APPLE_JWT_KID | The id of your private key. This is found in the management panel for your private key. |
+| APPLE_JWT_ISS | Your Apple Developer team id. |
+| APPLE_JWT_BYPASS_TOKEN | A token to use to bypass device check. This is helpful for development with the Xcode Simulator. (Optional) |
+
## 🔑 Setting up your App / Retrieving a DeviceCheck Token
You'll need to import Apple's `DeviceCheck` Framework to retrieve a token for your device.
diff --git a/Sources/VaporDeviceCheck/AppleDeviceCheckClient.swift b/Sources/VaporDeviceCheck/AppleDeviceCheckClient.swift
index 2d3f76e..859eb5e 100644
--- a/Sources/VaporDeviceCheck/AppleDeviceCheckClient.swift
+++ b/Sources/VaporDeviceCheck/AppleDeviceCheckClient.swift
@@ -1,19 +1,27 @@
import Vapor
import JWT
-public struct AppleDeviceCheckClient: DeviceCheckClient {
+public struct AppleDeviceCheckClient: DeviceCheckClient, Sendable {
public let jwkKid: JWKIdentifier
public let jwkIss: String
public func request(_ request: Request, deviceToken: String, isSandbox: Bool) -> EventLoopFuture {
- request.client.post(URI(string: "https://\(isSandbox ? "api.development" : "api").devicecheck.apple.com/v1/validate_device_token")) {
- $0.headers.add(name: .authorization, value: "Bearer \(try signedJwt(for: request))")
- return try $0.content.encode(DeviceCheckRequest(deviceToken: deviceToken))
+ let promise = request.eventLoop.makePromise(of: ClientResponse.self)
+
+ promise.completeWithTask {
+ var headers = HTTPHeaders()
+ headers.add(name: .authorization, value: "Bearer \(try await signedJwt(for: request))")
+
+ let response = try await request.client.post((URI(string: "https://\(isSandbox ? "api.development" : "api").devicecheck.apple.com/v1/validate_device_token")), headers: headers, content: DeviceCheckRequest(deviceToken: deviceToken))
+
+ return response
}
+
+ return promise.futureResult
}
- private func signedJwt(for request: Request) throws -> String {
- try request.jwt.sign(DeviceCheckJWT(iss: jwkIss), kid: jwkKid)
+ private func signedJwt(for request: Request) async throws -> String {
+ try await request.jwt.sign(DeviceCheckJWT(iss: jwkIss), kid: jwkKid)
}
}
@@ -21,7 +29,7 @@ private struct DeviceCheckJWT: JWTPayload {
let iss: String
let iat: Int = Int(Date().timeIntervalSince1970)
- func verify(using signer: JWTSigner) throws {
+ func verify(using algorithm: some JWTKit.JWTAlgorithm) async throws {
//no-op
}
}
diff --git a/Sources/VaporDeviceCheck/DeviceCheck.swift b/Sources/VaporDeviceCheck/DeviceCheck.swift
index d3de632..6000cfd 100644
--- a/Sources/VaporDeviceCheck/DeviceCheck.swift
+++ b/Sources/VaporDeviceCheck/DeviceCheck.swift
@@ -8,15 +8,18 @@ public struct NoAppleDeviceTokenError: DebuggableError {
public struct DeviceCheck: Middleware {
let excludes: [[PathComponent]]?
+ /// Tokens to include via environment to bypass device check. Designed for applications like the xcode simulator
+ let bypassTokens: Set
let client: DeviceCheckClient
- public init(jwkKid: JWKIdentifier, jwkIss: String, excludes: [[PathComponent]]? = nil, client: DeviceCheckClient? = nil) {
+ public init(jwkKid: JWKIdentifier, jwkIss: String, excludes: [[PathComponent]]? = nil, bypassTokens: Set = [], client: DeviceCheckClient? = nil) {
self.excludes = excludes
+ self.bypassTokens = bypassTokens
self.client = client ?? AppleDeviceCheckClient(jwkKid: jwkKid, jwkIss: jwkIss)
}
public func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture {
- requestDeviceCheck(on: request, chainingTo: next, isSandbox: false)
+ requestDeviceCheck(on: request, chainingTo: next, isSandbox: [Environment.development, Environment.testing].contains((try? Environment.detect()) ?? .production))
}
private func requestDeviceCheck(on request: Request, chainingTo next: Responder, isSandbox: Bool) -> EventLoopFuture {
@@ -27,6 +30,10 @@ public struct DeviceCheck: Middleware {
guard let xAppleDeviceToken = request.headers.first(name: .xAppleDeviceToken) else {
return request.eventLoop.makeFailedFuture(NoAppleDeviceTokenError())
}
+
+ if bypassTokens.contains(where: { $0 == (String(data: Data(base64Encoded: xAppleDeviceToken) ?? Data(), encoding: .utf8) ?? xAppleDeviceToken) }) {
+ return next.respond(to: request)
+ }
return client.request(request, deviceToken: xAppleDeviceToken, isSandbox: isSandbox)
.flatMap { res in