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