Sign in with Apple Using Vapor 4 | raywenderlich.com

In this Vapor 4 tutorial, you’ll learn how to implement Sign in with Apple with an iOS companion app and a simple website.


This is a companion discussion topic for the original entry at https://www.raywenderlich.com/11436647-sign-in-with-apple-using-vapor-4

Great article. Thank you!

I was looking into exchanging the authorization code for refresh token. The Vapor JWT Kit can be used to sign the JWT. However, Apple provides a p12 for signing these tokens. Do you know how to export the private key in EC format so that I can use it with the JWT Kit?

Nice article, I was only missing one thing, the protection of the API and enforcing authentication by using middleware.

Something like this:

let tokenProtectedAPI = unprotectedAPI.grouped(Token.authenticator()).grouped(User.guardMiddleware())

thanks for the tutorial! When I get the state out of the session, it’s always nil. I’m using req.session.data["state"] just like in the example.

It seems like the Vapor uses cookies to identify sessions, and Apple doesn’t send the cookie back with the callback. It seems like the state key should be the id of the cookie.

I feel like I’ve faithfully copy-pasted the session middleware setup from your tutorial, is there something I could be missing?

Thanks

Same thing for me :man_shrugging:

@duhfrazee @danramteke

With one of the recent updates to Vapor, the cookie same-site policy defaults to lax (previously it was none). Unfortunately lax is not sufficient as Apple is using POST requests for their redirection and lax only works with GET. You can find more information here: "Sign in with Apple" implementation hurdles - DEV Community 👩‍💻👨‍💻

:warning: Quick-fix - Insecure

A quick-fix would be to use your own SessionsConfiguration when initializing the SessionsMiddleware. Works for testing, but you shouldn’t do this on production (opens you up to CSRF attacks).

in configure.swift:

let sessionsMiddleware = SessionsMiddleware(
    session: app.sessions.driver,
    configuration: .init(
      cookieName: "SIWA",
      cookieFactory: { sessionID -> HTTPCookies.Value in
        return .init(
          string: sessionID.string,
          expires: Date(timeIntervalSinceNow: 60*10), // 10 minutes
          maxAge: nil,
          domain: nil,
          path: "/web/auth/siwa/",
          isSecure: false,
          isHTTPOnly: false,
          sameSite: HTTPCookies.SameSitePolicy.none
        )
      }
    )
  )
  app.middleware.use(sessionsMiddleware)

There is another solution that is not using the existing SessionsMiddleware (so you can still use it properly and securely for the rest of your site) posted by 0xTim in Vapor Discord:

:white_check_mark: Proper-fix - creating a separate Cookie just for SIWA

Create a separate middleware, which will check if there is a state in our session and copy it into a separate Cookie with same-site policy none:

import Vapor

struct SignInWithAppleStateMiddleware: Middleware {
    func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture<Response> {
        next.respond(to: request).map { response in
            if let state = request.session.data["state"] {
                let expiryDate = Date().addingTimeInterval(300)
                let cookie = HTTPCookies.Value(string: state, expires: expiryDate, maxAge: 300, isSecure: false, isHTTPOnly: true, sameSite: HTTPCookies.SameSitePolicy.none)
                response.cookies["state"] = cookie
            }
            return response
        }
    }
}

In configure.swift, add SignInWithAppleStateMiddleware after SessionsMiddleware:

app.middleware.use(SignInWithAppleStateMiddleware())

In SIWAViewController.swift, update callback(req) and replace the guard statement with:

guard
      let sessionState = req.cookies["state"]?.string,
      !sessionState.isEmpty,
      sessionState == auth.state else {
        return req.eventLoop.makeFailedFuture(UserError.siwaInvalidState)
    }

We will update the tutorial soon! :slight_smile:

When creating the key in the developer portal, you can download the auth key (.p8 file).

To sign you have to load it (bytes) and use:

let signer = try JWTSigner.es256(key: .private(pem: {{key in bytes}} ))

You could store the contents of the .p8 file base64 encoded in an env var and load it from there (using base 64 decode).

Best,
Christian

Thank you for the excellent article. Is there a way to implement it using core data?

Hi @panos2310,

Thanks for your feedback! :slight_smile:

Regarding Core Data: can you give a bit more context about what you want to achieve / use it for?

Thanks,
Christian

Thank you for you reply. I would like to be able to use core data entities instead of Flutter/SQL to store user information and tokens.