Chapter 24: Empty cookies dictionary when appleAuthCallbackHandler(_:)

I’m having trouble finding out what went wrong while following along with chapter 24 and implementing Sign In with Apple for the web.

When I click the Sign In with Apple button and follow the steps, I get redirected to the route https://<my-ngrok-domain>/login/siwa/callback, but the response is 401 Unauthorized.

image

Here’s the code where the problem exist:

func appleAuthCallbackHandler(_ request: Request) throws -> EventLoopFuture<View> {
        let siwaData = try request.content.decode(AppleAuthorizationResponse.self)
        request.logger.info(Logger.Message(stringLiteral: request.cookies.all.description))

        guard
            let sessionState = request.cookies["SIWA_STATE"]?.string,
            !sessionState.isEmpty,
            sessionState == siwaData.state
        else {
            request.logger.warning("SIWA does not exist or does not match")
            throw Abort(.unauthorized)
        }

        let context = SIWAHandleContext(
            token: siwaData.idToken,
            email: siwaData.user?.email,
            firstName: siwaData.user?.name?.firstName,
            lastName: siwaData.user?.name?.lastName
        )

        return request.view.render("siwaHandler", context)
    }

The guard breaks and the log shows:

[ INFO ] [:] [request-id: 6C9456EE-4A7A-4988-B3B0-79507B797C96] (App/Controllers/WebsiteController.swift:345)
[ WARNING ] SIWA does not exist or does not match [request-id: 6C9456EE-4A7A-4988-B3B0-79507B797C96] (App/Controllers/WebsiteController.swift:352)
[ WARNING ] Abort.401: Unauthorized [request-id: 6C9456EE-4A7A-4988-B3B0-79507B797C96] (App/Controllers/WebsiteController.swift:353)

Although the browser clearly shows the SIWA_STATE cookie exist, the log shows that the cookies dictionary is empty. What could be the reason for this?

If you put a breakpoint inside the else block, you can get the value of the session state and compare it to the state returned by Apple

Thank you for replying, @0xtim. I added the breakpoint at the else block and found po request.cookies is still empty:

(lldb) po request.cookies
▿ HTTPCookies
  - cookies : 0 elements

I’m guessing this is the reason why the else block is reached in the first place (because SIWA_STATE doesn’t exist at the point when the guard is reached). There is a value for siwaData.state at that point, but there is no value for SIWA_STATE to match against.

How do you create the SIWA_STATE cookie? i.e. what does your code look like

This is my SIWAContext:

struct SIWAContext: Encodable {
    let clientID: String
    let scopes: String
    let redirectURI: String
    let state: String
}

And this is my RegisterContext:

struct RegisterContext: Encodable {
    let title = "Register"
    let message: String?
    let siwaContext: SIWAContext

    init(message: String? = nil, siwaContext: SIWAContext) {
        self.message = message
        self.siwaContext = siwaContext
    }
}

Then I have this function:

private func buildSIWAContext(on request: Request) throws -> SIWAContext {
        let state = [UInt8].random(count: 32).base64

        let scopes = "name email"

        guard let clientID = Environment.get("WEBSITE_APPLICATION_IDENTIFIER") else {
            request.logger.error("WEBSITE_APPLICATION_IDENTIFIER not set")
            throw Abort(.internalServerError)
        }

        guard let redirectURI = Environment.get("SIWA_REDIRECT_URL") else {
            request.logger.error("SIWA_REDIRECT_URL not set")
            throw Abort(.internalServerError)
        }

        let siwa = SIWAContext(clientID: clientID, scopes: scopes, redirectURI: redirectURI, state: state)
        return siwa
    }

I use buildSIWAContext(on:) inside registerHandler(_:) like this:

func registerHandler(_ request: Request) throws -> EventLoopFuture<Response> {
        let siwaContext = try buildSIWAContext(on: request)
        let context: RegisterContext

        if let message = request.query[String.self, at: "message"] {
            context = RegisterContext(message: message, siwaContext: siwaContext)
        } else {
            context = RegisterContext(siwaContext: siwaContext)
        }

        return request.view
            .render("register", context)
            .encodeResponse(for: request)
            .map { response in
                let expiryDate = Date().addingTimeInterval(300)

                let cookie = HTTPCookies.Value(
                    string: siwaContext.state,
                    expires: expiryDate,
                    maxAge: 300,
                    isHTTPOnly: true,
                    sameSite: HTTPCookies.SameSitePolicy.none
                )

                response.cookies["SIWA_STATE"] = cookie

                return response
            }
    }

I tried to follow the book step-by-step. I’m not sure where things went wrong. I appreciate your time to help.

OK I discovered something that may be the cause. As soon as I click the Sign In with Apple button, the Chrome browser console prints the following error:

image

I’m guessing there’s something wrong with the way I wrote my script in register.leaf. I added this bellow as the book says:

<div id="appleid-signin" class="signin-button" data-color="black" data-border="true" data-type="sign in"></div>

<script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script>

<script type="text/javascript">
    AppleID.auth.init({
        clientId: '#(siwaContext.clientID)',
        scope: '#(siwaContext.scopes)',
        redirectURI: '#(siwaContext.redirectURI)',
        state: '#(siwaContext.state)',
        usePopup: false
    });
</script>

How do I fix this error?

When you say you’re following along with the chapter - are you following along exactly or are you trying to add your own stuff? I’m confused as to what app.js is? It looks like you’re setting a Content Security Policy unless Chrome is now doing that for you - is that right?

I created a vapor app since the start of the book and have been following along in the same app step-by-step. I didn’t encounter anything mentioning Content Security Policy and this is the first time I know about it.

Quick search led me to this thread in Apple Developer Forums where people are facing a similar issue and no one knows how to fix it: Sign In With Apple JS nonce error | Apple Developer Forums

I shared my issue there as well.

Does it work in Safari?

It doesn’t work in Safari. Although I’m not sure the same error is printed in Safari’s console, I still get the 401 Unauthorized response at the end. I’m not currently on my Mac that has the Vapor app but I will check it later today and confirm if it’s the same error or not.

@0xtim I just checked in Safari and the console doesn’t show anything. However, the original problem is still there. I end up with a 401 Unauthorized error when reaching /login/siwa/callback.

Digging further, I add a breakpoint at the first line of appleAuthCallbackHandler(_:) to inspect the Request passed to that function.

I noticed that Request has a computed property called cookies under vapor/Sources/Vapor/Request/Request.swift defined as such:

/// Get and set `HTTPCookies` for this `HTTPRequest`
/// This accesses the `"Cookie"` header.
public var cookies: HTTPCookies {
    get {
        return self.headers.cookie ?? .init()
    }
    set {
        self.headers.cookie = newValue
    }
}

As you can see, the getter uses nil coalescing to return a new instance of Request if self.headers.cookie is nil. So the next thing I did was to po request.headers.cookie directly and indeed the value is nil:

(lldb) po request.headers.cookie
nil

The question is now why is this value nil?

PS To be 100% sure that my code isn’t the problem, I also tested the final version of the TIL app for chapter 24 from the book’s materials. The same problem is also there.

I’m going to investigate this because I suspect Apple have changed something and broken it

1 Like

I have the same problem. Used Firefox and Safari.

  • I added some parameters to the cookie, but it not working.
    let cookie = HTTPCookies.Value(string: siwaContext.state, expires: expiryDate, maxAge: 300, domain: “apple.com”, path: “/login/siwa/callback”, isSecure: true, isHTTPOnly: true, sameSite: HTTPCookies.SameSitePolicy.none)
  • I tried to disable CSP in the browser, but it doesn’t help. Firefox: Type about:config in the Firefox address bar and find security.csp.enable and set it to false.
  • I suppose the problem in Vapor. Different requests don’t see cookies from each other.
  • Well, I commented the cookie check, I get empty email and name without any errors.

Hi all,

I’ve been looking in to this today and it’s a bug in Vapor. Basically a couple of months ago Vapor added support for better parsing of of HTTP headers to support complex accept headers (PR Here). That had the unintended side effect of breaking any headers with a date in, because valid cookie dates have a comma which we don’t currently account for. I’m working on a fix now.

2 Likes

Amazing!

Please share the PR for the fix here when you have a chance. I would love to learn from it.

Thank you

Great! I’m approaching this chapter and was hoping a fix would come soon!

Thanks for investigating and fixing!

Why is there a blank email address and name from apple, that doesn’t depend on the cookie?
P.S. Checking the cookie from a program request is successful. I don’t see a difference in the headers from a program request and the apple’s one.

It might be because the user has already signed in with the app? At which point you’re supposed to just look up the SIWA ID. I seem to remember that being a thing