Group Group Group Group Group Group Group Group Group

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.

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:

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: https://developer.apple.com/forums/thread/668286

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