Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Authorization Code + PKCE #1572

Closed
Renkas opened this issue Sep 20, 2019 · 25 comments
Closed

Authorization Code + PKCE #1572

Renkas opened this issue Sep 20, 2019 · 25 comments

Comments

@Renkas
Copy link

Renkas commented Sep 20, 2019

Describe the bug
I'm using simple native JS client from: https://github.com/aaronpk/pkce-vanilla-js to test my Hydra instance with Authorization Code + PKCE grant. Everything works fine untill the access_token request. I keep getting the following error:

{
"error":"invalid_grant",
"error_description":"The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client",
"error_hint":"The PKCE code challenge did not match the code verifier.",
"status_code":400
}

I'm pretty sure challenge and verifier I send to the server are correct. I have also verifed that these values are really sent to the server.

code_verifier: 6e5ec79163ee989be79fa7a542f599d0e7c77038c8620aa44170217c
code_challenge: ZTE4MTMxZGRlMjA2ZGJiZjAyNTMyZjU3NjM0MThmOTkxYTQ4Y2EyZjFhY2Y1NjRmOGI1YWE1ZjJhYTRkYTE3Ng
These values are being sent to the server and AFAIK they are totally correct. But I still get the error stating that these dont match. I have hit a wall on debugging this.

I also had a look into Hydra DB and the table hydra_oauth2_pkce is empty. Shouldn't session data from authorization code request be saved there? Maybe that's what is creating the problem? Is there something I can do from my side?

Environment

  • Version: v1.0.1
  • Environment: Docker
@aeneasr
Copy link
Member

aeneasr commented Sep 20, 2019

Is your client public or private?

@Renkas
Copy link
Author

Renkas commented Sep 20, 2019

Do you mean if the client has a secret set? No it has not. And token_endpoint_auth_method is set to none

@aeneasr
Copy link
Member

aeneasr commented Sep 20, 2019

Ok, could you show the full auth URL please, and the 302 redirect urls plus the last url? PKCE works fine. It also appears that here's a safari bug that could cause this: aaronpk/pkce-vanilla-js#1

@Renkas
Copy link
Author

Renkas commented Sep 20, 2019

Will do later. And this bug does not seem to be related to my case.

@aeneasr
Copy link
Member

aeneasr commented Sep 20, 2019

Also please include hydra logs with LOG_LEVEL=trace

@Renkas
Copy link
Author

Renkas commented Sep 20, 2019

http://hydra/oauth2/auth?response_type=code&client_id=JbLXcP5pgwiAqu-J&state=1c925009efe0631a8ba6f7f6874e3af00ff127aa34fac98aeba9dc59&scope=openid&redirect_uri=http%3A%2F%2Fapplication%2Ftools%2F&code_challenge=NTY3MzA4NjQzMDgyMzkwZmYwYjAwOGUwNTllNmIzYjdhY2JjZjgzZDM1MTEyNTIyNWJhMTUxMWM5YmQ4MjU5Zg&code_challenge_method=S256
http://application/login/?login_challenge=ead32870c24448e697478be9822443ac
http://hydra/oauth2/auth?client_id=JbLXcP5pgwiAqu-J&code_challenge=NTY3MzA4NjQzMDgyMzkwZmYwYjAwOGUwNTllNmIzYjdhY2JjZjgzZDM1MTEyNTIyNWJhMTUxMWM5YmQ4MjU5Zg&code_challenge_method=S256&login_verifier=0e362127fb83475fa65d95be3676c8ca&redirect_uri=http%3A%2F%2Fapplication%2Ftools%2F&response_type=code&scope=openid&state=1c925009efe0631a8ba6f7f6874e3af00ff127aa34fac98aeba9dc59
http://application/consent/?consent_challenge=a34f8e5e16694e87a4c8d1468b5ed34c
http://hydra/oauth2/auth?client_id=JbLXcP5pgwiAqu-J&code_challenge=NTY3MzA4NjQzMDgyMzkwZmYwYjAwOGUwNTllNmIzYjdhY2JjZjgzZDM1MTEyNTIyNWJhMTUxMWM5YmQ4MjU5Zg&code_challenge_method=S256&consent_verifier=c8e2fa9a632d41c8986684b91991a597&redirect_uri=http%3A%2F%2Fapplication%2Ftools%2F&response_type=code&scope=openid&state=1c925009efe0631a8ba6f7f6874e3af00ff127aa34fac98aeba9dc59
http://application/tools/?code=e7Ur-WS518pHqBodweXaz2lwvgt2LHQpg28DM2ze3LU.ozj2HqDXCzoj_eSmSMZ_MaUr4iMDLd8KnVKTPHdwfPg&scope=&state=1c925009efe0631a8ba6f7f6874e3af00ff127aa34fac98aeba9dc59

POST
http://hydra/oauth2/token
grant_type=authorization_code&code=e7Ur-WS518pHqBodweXaz2lwvgt2LHQpg28DM2ze3LU.ozj2HqDXCzoj_eSmSMZ_MaUr4iMDLd8KnVKTPHdwfPg&client_id=JbLXcP5pgwiAqu-J&redirect_uri=http://application/tools/&code_verifier=5f374b32740228a443220fc6f0ffb6dbb15f80326f0d6fa1f5220d2d
{
    "client_id": "JbLXcP5pgwiAqu-J",
    "client_name": "PKCE client",
    "redirect_uris": [
        "http://application/tools/"
    ],
    "grant_types": [
        "authorization_code"
    ],
    "response_types": [],
    "scope": "offline_access offline openid",
    "audience": [],
    "owner": "1",
    "policy_uri": "",
    "allowed_cors_origins": [],
    "tos_uri": "",
    "client_uri": "",
    "logo_uri": "",
    "contacts": [],
    "client_secret_expires_at": 0,
    "subject_type": "public",
    "token_endpoint_auth_method": "none",
    "userinfo_signed_response_alg": "none",
    "created_at": "2019-09-20T00:10:40Z",
    "updated_at": "2019-09-20T00:38:36Z"
}
docker_hydra | time="2019-09-20T11:57:40Z" level=info msg="started handling request" method=GET remote="172.18.0.1:44314" request="/oauth2/auth?response_type=code&client_id=JbLXcP5pgwiAqu-J&state=1c925009efe0631a8ba6f7f6874e3af00ff127aa34fac98aeba9dc59&scope=openid&redirect_uri=http%3A%2F%2Fapplication%2Ftools%2F&code_challenge=NTY3MzA4NjQzMDgyMzkwZmYwYjAwOGUwNTllNmIzYjdhY2JjZjgzZDM1MTEyNTIyNWJhMTUxMWM5YmQ4MjU5Zg&code_challenge_method=S256"
docker_hydra | time="2019-09-20T11:57:40Z" level=info msg="completed handling request" measure#hydra/public:http://hydra/.latency=31484000 method=GET remote="172.18.0.1:44314" request="/oauth2/auth?response_type=code&client_id=JbLXcP5pgwiAqu-J&state=1c925009efe0631a8ba6f7f6874e3af00ff127aa34fac98aeba9dc59&scope=openid&redirect_uri=http%3A%2F%2Fapplication%2Ftools%2F&code_challenge=NTY3MzA4NjQzMDgyMzkwZmYwYjAwOGUwNTllNmIzYjdhY2JjZjgzZDM1MTEyNTIyNWJhMTUxMWM5YmQ4MjU5Zg&code_challenge_method=S256" status=302 text_status=Found took=31.484ms
docker_hydra | time="2019-09-20T11:57:40Z" level=info msg="started handling request" method=PUT remote="172.18.0.4:44398" request="/oauth2/auth/requests/login/accept?login_challenge=ead32870c24448e697478be9822443ac"
docker_hydra | time="2019-09-20T11:57:40Z" level=info msg="completed handling request" measure#hydra/admin: http://hydra/.latency=11977400 method=PUT remote="172.18.0.4:44398" request="/oauth2/auth/requests/login/accept?login_challenge=ead32870c24448e697478be9822443ac" status=200 text_status=OK took=11.9774ms
docker_hydra | time="2019-09-20T11:57:40Z" level=info msg="started handling request" method=GET remote="172.18.0.1:44314" request="/oauth2/auth?client_id=JbLXcP5pgwiAqu-J&code_challenge=NTY3MzA4NjQzMDgyMzkwZmYwYjAwOGUwNTllNmIzYjdhY2JjZjgzZDM1MTEyNTIyNWJhMTUxMWM5YmQ4MjU5Zg&code_challenge_method=S256&login_verifier=0e362127fb83475fa65d95be3676c8ca&redirect_uri=http%3A%2F%2Fapplication%2Ftools%2F&response_type=code&scope=openid&state=1c925009efe0631a8ba6f7f6874e3af00ff127aa34fac98aeba9dc59"
docker_hydra | time="2019-09-20T11:57:40Z" level=info msg="completed handling request" measure#hydra/public:http://hydra/.latency=27808400 method=GET remote="172.18.0.1:44314" request="/oauth2/auth?client_id=JbLXcP5pgwiAqu-J&code_challenge=NTY3MzA4NjQzMDgyMzkwZmYwYjAwOGUwNTllNmIzYjdhY2JjZjgzZDM1MTEyNTIyNWJhMTUxMWM5YmQ4MjU5Zg&code_challenge_method=S256&login_verifier=0e362127fb83475fa65d95be3676c8ca&redirect_uri=http%3A%2F%2Fapplication%2Ftools%2F&response_type=code&scope=openid&state=1c925009efe0631a8ba6f7f6874e3af00ff127aa34fac98aeba9dc59" status=302 text_status=Found took=27.8084ms
docker_hydra | time="2019-09-20T11:57:40Z" level=info msg="started handling request" method=PUT remote="172.18.0.4:44406" request="/oauth2/auth/requests/consent/accept?consent_challenge=a34f8e5e16694e87a4c8d1468b5ed34c"
docker_hydra | time="2019-09-20T11:57:40Z" level=info msg="completed handling request" measure#hydra/admin: http://hydra/.latency=10873700 method=PUT remote="172.18.0.4:44406" request="/oauth2/auth/requests/consent/accept?consent_challenge=a34f8e5e16694e87a4c8d1468b5ed34c" status=200 text_status=OK took=10.8737ms
docker_hydra | time="2019-09-20T11:57:40Z" level=info msg="started handling request" method=GET remote="172.18.0.1:44314" request="/oauth2/auth?client_id=JbLXcP5pgwiAqu-J&code_challenge=NTY3MzA4NjQzMDgyMzkwZmYwYjAwOGUwNTllNmIzYjdhY2JjZjgzZDM1MTEyNTIyNWJhMTUxMWM5YmQ4MjU5Zg&code_challenge_method=S256&consent_verifier=c8e2fa9a632d41c8986684b91991a597&redirect_uri=http%3A%2F%2Fapplication%2Ftools%2F&response_type=code&scope=openid&state=1c925009efe0631a8ba6f7f6874e3af00ff127aa34fac98aeba9dc59"
docker_hydra | time="2019-09-20T11:57:40Z" level=info msg="completed handling request" measure#hydra/public:http://hydra/.latency=23339900 method=GET remote="172.18.0.1:44314" request="/oauth2/auth?client_id=JbLXcP5pgwiAqu-J&code_challenge=NTY3MzA4NjQzMDgyMzkwZmYwYjAwOGUwNTllNmIzYjdhY2JjZjgzZDM1MTEyNTIyNWJhMTUxMWM5YmQ4MjU5Zg&code_challenge_method=S256&consent_verifier=c8e2fa9a632d41c8986684b91991a597&redirect_uri=http%3A%2F%2Fapplication%2Ftools%2F&response_type=code&scope=openid&state=1c925009efe0631a8ba6f7f6874e3af00ff127aa34fac98aeba9dc59" status=302 text_status=Found took=23.3399ms
docker_hydra | time="2019-09-20T11:57:40Z" level=info msg="started handling request" method=POST remote="172.18.0.1:44338" request=/oauth2/token
docker_hydra | time="2019-09-20T11:57:40Z" level=error msg="An error occurred" description="The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client" error=invalid_grant hint="The PKCE code challenge did not match the code verifier."
docker_hydra | time="2019-09-20T11:57:40Z" level=debug msg="Stack trace: \ngithub.jparrowsec.cn/ory/fosite/handler/pkce.(*Handler).HandleTokenEndpointRequest\n\t/go/pkg/mod/github.com/ory/[email protected]/handler/pkce/handler.go:210\ngithub.jparrowsec.cn/ory/fosite.(*Fosite).NewAccessRequest\n\t/go/pkg/mod/github.com/ory/[email protected]/access_request_handler.
go:89\ngithub.jparrowsec.cn/ory/hydra/oauth2.(*Handler).TokenHandler\n\t/go/src/github.com/ory/hydra/oauth2/handler.go:548\nnet/http.HandlerFunc.ServeHTTP\n\t/usr/local/go/src/net/http/server.go:1995\ngithub.jparrowsec.cn/julienschmidt/httprouter.(*Router).Handler.func1\n\t/go/pkg/mod/github.com/julienschmidt/[email protected]/params_go17.go:26\ngithub.jparrowsec.cn/julienschmidt/httprouter.(*Router).ServeHTTP\n\t/go/pkg/mod/github.com/julienschmidt/[email protected]/router.go:334\ngithub.jparrowsec.cn/urfave/negroni.Wrap.func1\n\t/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:46\ngithub.jparrowsec.cn/urfave/negroni.HandlerFunc.ServeHTTP\n\t/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:29\ngithub.jparrowsec.cn/urfave/negroni.middleware.ServeHTTP\n\t/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:38\nnet/http.HandlerFunc.ServeHTTP\n\t/usr/local/go/src/net/http/server.go:1995\ngithub.jparrowsec.cn/ory/hydra/x.RejectInsecureRequests.func1\n\t/go/src/github.com/ory/hydra/x/tls_termination.go:55\ngithub.jparrowsec.cn/urfave/negroni.HandlerFunc.ServeHTTP\n\t/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:29\ngithub.jparrowsec.cn/urfave/negroni.middleware.ServeHTTP\n\t/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:38\ngithub.jparrowsec.cn/ory/x/metricsx.(*Service).ServeHTTP\n\t/go/pkg/mod/github.com/ory/[email protected]/metricsx/middleware.go:261\ngithub.jparrowsec.cn/urfave/negroni.middleware.ServeHTTP\n\t/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:38\ngithub.jparrowsec.cn/ory/hydra/metrics/prometheus.(*MetricsManager).ServeHTTP\n\t/go/src/github.com/ory/hydra/metrics/prometheus/middleware.go:26\ngithub.jparrowsec.cn/urfave/negroni.middleware.ServeHTTP\n\t/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:38\ngithub.jparrowsec.cn/meatballhat/negroni-logrus.(*Middleware).ServeHTTP\n\t/go/pkg/mod/github.com/meatballhat/[email protected]/middleware.go:136\ngithub.jparrowsec.cn/urfave/negroni.middleware.ServeHTTP\n\t/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:38\ngithub.jparrowsec.cn/urfave/negroni.(*Negroni).ServeHTTP\n\t/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:96\nnet/http.serverHandler.ServeHTTP\n\t/usr/local/go/src/net/http/server.go:2774\nnet/http.(*conn).serve\n\t/usr/local/go/src/net/http/server.go:1878\nruntime.goexit\n\t/usr/local/go/src/runtime/asm_amd64.s:1337"
docker_hydra | time="2019-09-20T11:57:40Z" level=info msg="completed handling request" measure#hydra/public:http://hydra/.latency=21300400 method=POST remote="172.18.0.1:44338" request=/oauth2/token status=400 text_status="Bad Request" took=21.3004ms

@aeneasr
Copy link
Member

aeneasr commented Sep 20, 2019

I think there is a problem with the persiting of the PKCE verifier. If you do:

base64(sha256(5f374b32740228a443220fc6f0ffb6dbb15f80326f0d6fa1f5220d2d))

you get

> base64(b883a96b4528a29b5fdbe70007248fa7fec3c9d6)
> Yjg4M2E5NmI0NTI4YTI5YjVmZGJlNzAwMDcyNDhmYTdmZWMzYzlkNg==

which does not match your initial challenge:

NTY3MzA4NjQzMDgyMzkwZmYwYjAwOGUwNTllNmIzYjdhY2JjZjgzZDM1MTEyNTIyNWJhMTUxMWM5YmQ4MjU5Zg

so the code (hydra) appears to behave correctly

@aeneasr
Copy link
Member

aeneasr commented Sep 20, 2019

you can probably debug this by console.log the verifier and see if it changes between requests

@Renkas
Copy link
Author

Renkas commented Sep 20, 2019

base64(sha256(5f374b32740228a443220fc6f0ffb6dbb15f80326f0d6fa1f5220d2d)) gives me NTY3MzA4NjQzMDgyMzkwZmYwYjAwOGUwNTllNmIzYjdhY2JjZjgzZDM1MTEyNTIyNWJhMTUxMWM5YmQ4MjU5Zg

I have no idea how you get Yjg4M2E5NmI0NTI4YTI5YjVmZGJlNzAwMDcyNDhmYTdmZWMzYzlkNg

@Renkas
Copy link
Author

Renkas commented Sep 20, 2019

seems you used sha1 not sha256.

@aeneasr
Copy link
Member

aeneasr commented Sep 20, 2019

Oh yeah, my bad. The values you posted are indeed correct (PKCE does not use padding). I traced the error and it appears to stem from the default case:

https://github.com/ory/fosite/blob/master/handler/pkce/handler.go#L206-L210

Which leads me to believe that the method is empty, which is weird, because it should be set during the first step of the PKCE flow and it should be retrieved here:

https://github.com/ory/fosite/blob/master/handler/pkce/handler.go#L133

It should further be set correctly because we do not allow the plain method as you can see here:

https://github.com/ory/fosite/blob/master/handler/pkce/handler.go#L99-L110

So this should be set correctly. Additionally, the PKCE session does/should properly store the challenge method as you can see here:

https://github.com/ory/fosite/blob/master/handler/pkce/handler.go#L65-L70

Which database adapter are you using? in memory or sql?

@aeneasr
Copy link
Member

aeneasr commented Sep 20, 2019

Whoops, my analysis was incorrect, I was looking at master instead of v0.29.7 - in that case fosite complains because the challenge computed form the verifier does not match the challenge itself:

https://github.com/ory/fosite/blob/v0.29.7/handler/pkce/handler.go#L209-L212

however, I see that fosite expects the code_verifier to be base64 encoded as well, which does not appear to be the case in your example:

https://github.com/ory/fosite/blob/v0.29.7/handler/pkce/handler.go#L199-L202

So this would expect

NWYzNzRiMzI3NDAyMjhhNDQzMjIwZmM2ZjBmZmI2ZGJiMTVmODAzMjZmMGQ2ZmExZjUyMjBkMmQ

instead of:

5f374b32740228a443220fc6f0ffb6dbb15f80326f0d6fa1f5220d2d

If I'm not mistaken, this is defined in the spec here: https://tools.ietf.org/html/rfc7636#section-4.1

NOTE: The code verifier SHOULD have enough entropy to make it
impractical to guess the value. It is RECOMMENDED that the output of
a suitable random number generator be used to create a 32-octet
sequence. The octet sequence is then base64url-encoded to produce a
43-octet URL safe string to use as the code verifier.

@aeneasr
Copy link
Member

aeneasr commented Sep 20, 2019

Hm, I'm getting a bit unsure about this section:

NOTE: The code verifier SHOULD have enough entropy to make it
impractical to guess the value. It is RECOMMENDED that the output of
a suitable random number generator be used to create a 32-octet
sequence. The octet sequence is then base64url-encoded to produce a
43-octet URL safe string to use as the code verifier.

I think this only means that that's one of the recommended ways how to generate a verifier, not that the verifier should generally be urldecoded before computing the challenge in the oauth2 server.

I'm not entirely sure why noone has noticed this yet. PKCE is available since forever. Could you maybe help digging up some libraries (except the one posted) to see how they behave? We obviously want to do this in a way that works with the ecosystem best and hast the fewest breaking changes.

@aeneasr
Copy link
Member

aeneasr commented Sep 20, 2019

Sorry for the comment flood, I'm a bit puzzled why this has come up just as of now. I remember quite a lot of people working with PKCE without issues.

The node-oidc-provider only checks for the character set, it does not decode the challenge:

https://github.com/panva/node-oidc-provider/blob/4ac3905aac5a32c9dc2ccba29975f68587bf77b2/lib/helpers/pkce.js#L20

@aeneasr aeneasr added bug Something is not working. package/oauth2 labels Sep 20, 2019
@aeneasr aeneasr modified the milestones: v1.0.1, v1.0.2 Sep 20, 2019
@Renkas
Copy link
Author

Renkas commented Sep 22, 2019

What about the table hydra_oauth2_pkce being empty? Isn't this the problem that data is not saved in that table for some reason and thats why the verification fail - because there is nothing to verify?

I'm using MySQL for database (v8.0.17)

@aeneasr
Copy link
Member

aeneasr commented Sep 23, 2019

Ok, so I had some time to properly look into the code. My previous assumption was wrong - PKCE is handled correctly by fosite which is also the reason why noone complained about the implementation of PKCE so far. I was confused by these lines:

https://github.com/ory/fosite/blob/v0.29.7/handler/pkce/handler.go#L187-L202

However, those are there to ensure that the entropy is high enough - they are not actually used for the verification process itself.

I also checked why hydra_oauth2_pkce appears to be empty. The reason is that we're deleting the row from the database right after fetching it and before the validation process. The idea behind this is that the PKCE challenge is valid only once, you can not re-do it. To see the entry, you would have to check the hydra_oauth2_pkce table BEFORE executing the /oauth2/token request.

Would it be possible for you to share a git repo with me that I can use for reproducing the issue? I'm now really clueless as to why this does not work.

@aeneasr aeneasr removed the bug Something is not working. label Sep 23, 2019
@aeneasr aeneasr removed this from the v1.0.2 milestone Sep 23, 2019
@Renkas
Copy link
Author

Renkas commented Sep 23, 2019

I seem to have gotten to the core of the problem - kind of. Seems like there is a problem with Chrome and XMLHttpRequest POST request on non-secure HTTP connection. I see from Inspector that correct data is being sent to the server but maybe some security layer in Chrome will block it? Or maybe it's because I'm using local domain with custom port and that will trigger some security layer shenanigans.

I tried now same test application on Firefox and it works fine there.

Thanks for you time. This was kind of odd thing to debug as everything seems to be alright on every step of the process. Even Chrome Inspector shows that correct data is being sent to the server. But I guess it's a kind of bug on their side? Or maybe a feature that is not well communicated in the Inspector.

I dont really have the time to go digging in Chromes internals/issue tracker if this is already reported or expected behavior. But if someone has any knowledge of that then please let me know.

@aeneasr
Copy link
Member

aeneasr commented Sep 23, 2019

It is possible that chrome or a privacy plugin is blocking localStore or some cookie required for the vanilljas repo to function properly

@aeneasr
Copy link
Member

aeneasr commented Sep 23, 2019

I'm closing this because it is not an issue with this repo

@aeneasr aeneasr closed this as completed Sep 23, 2019
@Renkas
Copy link
Author

Renkas commented Sep 23, 2019

I dont think so. I don't have any plugins that could do that.

Also localStorage is working fine. I see from Inspector what data is being sent to the server and it is all correct. It just seems that there is some layer after Chrome inspector that does something to the request.

Just try it in Chrome with HTTP connection: https://github.com/aaronpk/pkce-vanilla-js
You need to implement your own sha256 though as window.crypto.subtle is not available on HTTP. If you are interested on testing this out then there is my full code that works on HTTP - I just changed the domain name and removed client_id: https://pastebin.com/HEy8tdmT

@nacidai
Copy link

nacidai commented Dec 13, 2019

The problem is in these lines of code:

	if base64.RawURLEncoding.EncodeToString(hash.Sum([]byte{})) != challenge {
		return errors.WithStack(fosite.ErrInvalidGrant.
			WithHint("The PKCE code challenge did not match the code verifier."))
	}

The "hash.Sum" returns byte array of RAW digest not HEX encoded hash, therefore base64.RawURLEncoding.EncodeToString does not calculate a proper base64 value. You can test it with the following code

	verifierSum := hash.Sum([]byte{})
	fmt.Printf("VerifyHash: %s\n", hex.EncodeToString(verifierSum))
	base64FromHex := base64.RawURLEncoding.EncodeToString([]byte(hex.EncodeToString(verifierSum)))
	fmt.Printf("Verifier computed challenge: %s\n",string(base64FromHex))
	base64FromRaw := base64.RawURLEncoding.EncodeToString(verifierSum)
	fmt.Printf("Verifier computed challenge: %s\n",string(base64FromRaw))

This would mean either the client uses sha256 hash using raw byte values (not hexed), or this code switches to HEX from RAW bytes

@aeneasr
Copy link
Member

aeneasr commented Dec 13, 2019

Are you saying that the implementation in the code (fosite) is incorrect, or that the client library is handling this improperly?

@aeneasr
Copy link
Member

aeneasr commented Dec 13, 2019

The "hash.Sum" returns byte array of RAW digest not HEX encoded hash, therefore base64.RawURLEncoding.EncodeToString does not calculate a proper base64 value. You can test it with the following code

While that's correct, base64 takes raw digest, not hex encoded strings. That's the whole idea of base64. Hex encoded is already an encoding, so why would we need to base64 it, there is no good reason to do so.

@nacidai
Copy link

nacidai commented Dec 13, 2019

The fosite implementation is correct in my opinion i.e. "BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))". The spec does not clearly state the following section, which confused me:
BASE64URL-ENCODE( <--raw bytes or hex?-- SHA256(ASCII(code_verifier)))

I hope this save someone some time. Most documentation assumes that it is implicit in the use of base64.

Note - I have updated my comment from two years ago (I did not notice a typo that might have caused further confusion rather than help people) -

@jaylinski
Copy link
Contributor

The spec has an example function in the appendix, which takes bytes (and not hex):

Appendix A.  Notes on Implementing Base64url Encoding without Padding

   This appendix describes how to implement a base64url-encoding
   function without padding, based upon the standard base64-encoding
   function that uses padding.

   To be concrete, example C# code implementing these functions is shown
   below.  Similar code could be used in other languages.

     static string base64urlencode(byte [] arg)
     {
       string s = Convert.ToBase64String(arg); // Regular base64 encoder
       s = s.Split('=')[0]; // Remove any trailing '='s
       s = s.Replace('+', '-'); // 62nd char of encoding
       s = s.Replace('/', '_'); // 63rd char of encoding
       return s;
     }

So I think the implementation is correct.

https://tools.ietf.org/html/rfc7636#appendix-A

I'm writing this because I'm experiencing the same error, and I'm pretty sure it's because my application overwrites the verifier (saved in the session) when multiple OAuth2-flows are executed in parallel. So after lots of debugging I'm pretty confident to say that Hydras implementation is correct.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants