vapor-community / Imperial

Federated Authentication with OAuth providers
MIT License
153 stars 48 forks source link

Missing code key in url query #89

Closed m-y-n-o-n-a closed 1 year ago

m-y-n-o-n-a commented 2 years ago

Triggered by this code

this is the code in Imperial/FederatedServiceRouter that throws:

` public func fetchToken(from request: Request) throws -> EventLoopFuture { let code: String if let queryCode: String = try request.query.get(at: codeKey) { code = queryCode } else if let error: String = try request.query.get(at: errorKey) { throw Abort(.badRequest, reason: error) } else { throw Abort(.badRequest, reason: "Missing 'code' key in URL query") }

    let body = callbackBody(with: code)
    let url = URI(string: accessTokenURL)

    return body.encodeResponse(for: request)
        .map { $0.body.buffer }
        .flatMap { buffer in
            return request.client.post(url, headers: self.callbackHeaders) { $0.body = buffer }
        }.flatMapThrowing { response in
            return try response.content.get(String.self, at: ["access_token"])
        }
}`

Known issue people had already before:

Post here how sb fixed it

Alright. I think I figured it out... What's the chance the data is being serialized as JSON even though I set static var defaultContentType: HTTPMediaType = .urlEncodedForm in LinkedInCallbackBody? The chance ended up being pretty high, given that adding a client.headers = HTTPHeaders(dictionaryLiteral: ("Content-Type", "application/x-www-form-urlencoded")) to the beforeSend closure eliminated the issue I was having. That, or Vapor doesn't automatically set that header... which would make more sense.

public func fetchToken(from request: Request) throws -> EventLoopFuture<String> {
    let code: String
    if let queryCode: String = try request.query.get(at: "code") {
        code = queryCode
    } else if let error: String = try request.query.get(at: "error") {
        throw Abort(.badRequest, reason: error)
    } else {
        throw Abort(.badRequest, reason: "Missing 'code' key in URL query")
    }

    let body = LinkedInCallbackBody(code: code, clientId: self.tokens.clientID, clientSecret: self.tokens.clientSecret, redirectURI: self.callbackURL)

    let url = URI(string: self.accessTokenURL)
    return body.encodeResponse(for: request).map {
        $0.body
    }.flatMap { body in
        return request.client.post(url, beforeSend: { client in
            client.body = body.buffer
            client.headers = HTTPHeaders(dictionaryLiteral: ("Content-Type", "application/x-www-form-urlencoded"))
        })
    }.flatMapThrowing { response in
        return try response.content.get(String.self, at: ["access_token"])
    }
}

<@!432065887202181142> This is what the (working) method ended up being. I'm willing to share my Imperial+LinkedIn code, if you think that'd be of any use.

I'm not quite good/confident enough with this to feel 100% comfortable submitting a PR, but maybe I should anyway...

public func fetchToken(from request: Request) throws -> EventLoopFuture<String> {
    let code: String
    if let queryCode: String = try request.query.get(at: "code") {
        code = queryCode
    } else if let error: String = try request.query.get(at: "error") {
        throw Abort(.badRequest, reason: error)
    } else {
        throw Abort(.badRequest, reason: "Missing 'code' key in URL query")
    }

    let body = LinkedInCallbackBody(code: code, clientId: self.tokens.clientID, clientSecret: self.tokens.clientSecret, redirectURI: self.callbackURL)

    let url = URI(string: self.accessTokenURL)
    return body.encodeResponse(for: request).map {
        $0.body
    }.flatMap { body in
        return request.client.post(url, beforeSend: { client in
            client.body = body.buffer
            client.headers = HTTPHeaders(dictionaryLiteral: ("Content-Type", "application/x-www-form-urlencoded"))
        })
    }.flatMapThrowing { response in
        return try response.content.get(String.self, at: ["access_token"])
    }
}

<@!432065887202181142> This is what the (working) method ended up being. I'm willing to share my Imperial+LinkedIn code, if you think that'd be of any use.

I'm not quite good/confident enough with this to feel 100% comfortable submitting a PR, but maybe I should anyway...

public func fetchToken(from request: Request) throws -> EventLoopFuture<String> {
    let code: String
    if let queryCode: String = try request.query.get(at: "code") {
        code = queryCode
    } else if let error: String = try request.query.get(at: "error") {
        throw Abort(.badRequest, reason: error)
    } else {
        throw Abort(.badRequest, reason: "Missing 'code' key in URL query")
    }

    let body = LinkedInCallbackBody(code: code, clientId: self.tokens.clientID, clientSecret: self.tokens.clientSecret, redirectURI: self.callbackURL)

    let url = URI(string: self.accessTokenURL)
    return body.encodeResponse(for: request).map {
        $0.body
    }.flatMap { body in
        return request.client.post(url, beforeSend: { client in
            client.body = body.buffer
            client.headers = HTTPHeaders(dictionaryLiteral: ("Content-Type", "application/x-www-form-urlencoded"))
        })
    }.flatMapThrowing { response in
        return try response.content.get(String.self, at: ["access_token"])
    }
}

<@!432065887202181142> This is what the (working) method ended up being. I'm willing to share my Imperial+LinkedIn code, if you think that'd be of any use.

I'm not quite good/confident enough with this to feel 100% comfortable submitting a PR, but maybe I should anyway...

I cannot fix this in the source code but as I saw other people are having the same issue, maybe this bug could be fixed.

0xTim commented 2 years ago

What version of Imperial do you have in your Package.swift and what does your setup code look like? I've just built an app using Sign in With Google and it's working for me

m-y-n-o-n-a commented 2 years ago

Hi Tim,

thank you for having a look into this.

I am basically following exactly the code in the book but the processGoogleLogin func is never reached.

Code attached with configuration file.

Kr, Andreas

Secured Email (ProtonMail).

------- Original Message ------- Tim Condon @.***> schrieb am Montag, 4. April 2022 um 19:16:

What version of Imperial do you have in your Package.swift and what does your setup code look like? I've just built an app using Sign in With Google and it's working for me

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you authored the thread.Message ID: @.***>

mynona commented 2 years ago

configure.swift.zip ImperialRouter+Controller.swift.zip

mynona commented 2 years ago

Package

// swift-tools-version:5.5 import PackageDescription

.package(url: "https://github.com/vapor-community/Imperial.git", from: "1.1.0"),

.product(name: "ImperialGoogle", package: "Imperial"),

fetched version: 1.1.0

0xTim commented 2 years ago

So my guess is that the app isn't configured correctly in the Google console. Do you get any information in the browser logs etc?

0xTim commented 2 years ago

And have you run a package update to make sure you're on the latest code?

m-y-n-o-n-a commented 2 years ago

And have you run a package update to make sure you're on the latest code?

Sure

mynona commented 2 years ago

Tim, I was able to catch the following errors. What does it mean?

[Error] Unrecognized Content-Security-Policy directive 'require-trusted-types-for'.

[Error] Unrecognized Content-Security-Policy directive 'require-trusted-types-for'. (x2)

[Error] Unrecognized Content-Security-Policy directive 'worker-src'.

[Error] Unrecognized Content-Security-Policy directive 'require-trusted-types-for'.

[Error] Unrecognized Content-Security-Policy directive 'require-trusted-types-for'.

[Error] Unrecognized Content-Security-Policy directive 'require-trusted-types-for'. (x2)

mynona commented 2 years ago

But this error is only shown when I try it for the second time and I am already logged in to google in this browser window

mynona commented 2 years ago

I re-checked all data in the google account and it is exactly done as described in the vapor book

mynona commented 2 years ago

but maybe I miss something.

0xTim commented 2 years ago

Hmm so I can't reproduce. What I suggest you do is debug the request/responses that happen during the OAuth handshake and see what the responses returned from Google are etc

mynona commented 2 years ago

how can i see the google responses?

Sent from ProtonMail for iOS

Am Sa., Apr. 9, 2022 um 20:19, Tim Condon @.***> schrieb:

Hmm so I can't reproduce. What I suggest you do is debug the request/responses that happen during the OAuth handshake and see what the responses returned from Google are etc

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you commented.Message ID: @.***>

0xTim commented 2 years ago

Put breakpoints in the code in Imperial and use the debugger to inspect what requests or responses Google makes

m-y-n-o-n-a commented 2 years ago

Tim, am I missing something regarding Cors. Does this have an effect on Imperial?

https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#requests_with_credentials

0xTim commented 2 years ago

I've implemented Imperial in a few projects recently and not hit this issue. Is it still an issue?

CORS is not an issue for this library

mynona commented 1 year ago

@0xTim I tried it again and posted it also in the Vapor discord channel.

Update: Now everything works well when I run it locally.

As soon as I run it on it in the production environment I get this error.

[ WARNING ] Abort.400: Missing 'code' key in URL query [request-id: 9EA82C34-50CB-4293-82D5-91312438C7CC] (ImperialCore/Routing/FederatedServiceRouter.swift:100)

https://discord.com/channels/431917998102675485/443878065227956226/1069207958724038807

I spend a few days in January last year, then tried it again from March to June as you can see. Now I picked this issue up again and the success is that it runs locally but I cannot make it work on the server due to this issue.

The environment variables are correct.

Google itself runs through and locally I also get the correct payload without any issues:

{ "id": "109655352539856189779", "email": "email", "verified_email": true, "name": "some name", "given_name": "some", "family_name": "name", "picture": "https://lh3.googleusercontent.com/a/blabla", "locale": "en-GB" }

Maybe you can point me in the right direction what I am doing wrong.

Thank you!!!

mynona commented 1 year ago

Found the root cause:

Following the request initiator chain in google https://www.mywebsite.com/auth/google/ -> https://accounts.google.com/o/oauth2/auth?client_id=EVERYTHING_CORRECT_HERE -> https://www.mywebsite.com/oauth/google?code=PROPER_CODE_HERE_AS_WELL -> https://www.mywebsite.com/oauth/google/

I discovered that there is a moment where the code gets lost.

The problem was caused by this middleware that extends url paths with "/"

struct ExtendPathMiddleware: AsyncMiddleware {

func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response { if !request.url.path.hasSuffix("/") && !request.url.path.contains(".") { return request.redirect(to: request.url.path + "/", type: .permanent) } return try await next.respond(to: request) } }

Put it here in case someone else searches for this and close the issue.

@0xTim thanks again for your ongoing support. Really hard to find the root cause sometimes.

mynona commented 1 year ago

ISSUE CAN BE CLOSED