Multiple OIDC identifiers for an identity don't work
noells opened this issue · comments
Preflight checklist
- I could not find a solution in the existing issues, docs, nor discussions.
- I agree to follow this project's Code of Conduct.
- I have read and am following this repository's Contribution Guidelines.
- I have joined the Ory Community Slack.
- I am signed up to the Ory Security Patch Newsletter.
Ory Network Project
No response
Describe the bug
One of the providers we use with Kratos is Sign in With Apple. Because of business reasons, we need to go over an App Transfer in apple. Basically, we need to switch from one account to the other. For this process, Apple provides a mechanism that allows you to generate the new sub
that they will use to log in with Kratos. From Kratos perspective, we need to change the secrets for apple OIDC provider. Basically, what we need to do is replace the apple identifier with the new identifier issued by Apple for all apple identities.
What I was trying to do was to include both identifiers (let's call them old_sub
and new_sub
) in the identities as this would guarantee a downtime free migration. This, however, does not work properly in Kratos side. Let me share the problems I've found:
- The HTTP api does not allow to import credentials to existing identities. The documentation claims it is possible but I couldn't do this using any of the endpoints in the docs.
- As the API does not allow it, I went directly to DB level. As Kratos is stateless, I thought this would work. While I can include all the information in the DB and all HTTP requests for the identity work well (I can even see both identifiers in the credentials secion). It is not possible to log in with any of the identifiers later on. I get this error:
{"audience":"application","error":{"debug":"Unable to find credentials that match the given provider \"google\" and subject \"9876\".","message":"An internal server error occurred, please contact the system administrator","reason":"Unable to find matching OpenID Connect Credentials.","stack_trace":"\ngithub.com/ory/kratos/selfservice/strategy/oidc.(*Strategy).processLogin\n\t/project/selfservice/strategy/oidc/strategy_login.go:180\ngithub.com/ory/kratos/selfservice/strategy/oidc.
...
What I've found out is that I can replace the old_sub
with the new_sub
and the log in, in this scenario, works perfectly.
While the workaround of replacing one identifier with the other works, it does not guarantee a downtime free migration in our case. If both were supported, it is pretty easy to realise that while Kratos is running with the old secrets, the old_sub
would work and, from the moment Kratos secrets are updated, the new_sub
would work.
As this is not the case, downtime free is not guarantee here. This is a huge problem for us, as we have millions of users that could be potentially affected by the changes.
Is there any reason why an identity cannot have multiple identifiers for the same OIDC provider? This could be related with #3909 , where linking accounts with the same OIDC provider is not allowed either. Is there a reason why this kind of operations are not allowed?
Reproducing the bug
The easiest way to reproduce this is the following:
- Log in with one OIDC account. Access the Kratos identity and save the credential identifier
- Delete account from step 1
- Log in with a different OIDC account from step 1 but for the same OIDC provider
- Patch the new account adding the identifier saved from step 1. You can patch it by running
#1st step
UPDATE identity_credentials SET config="{\"providers\":[{\"initial_access_token\":\"\",\"initial_id_token\":\"\",\"initial_refresh_token\":\"\",\"provider\":\"apple\",\"subject\":\"${newSub}\"}]}"
WHERE identity_id = YOUR_IDENTITY_ID_FROM_STEP_3
#2nd step
UPDATE identity_credential_identifiers SET identifier=${new_sub}
WHERE identity_credential_id = CREDENTIAL_ID_UPDATED_IN_SQL_1stSTEP
- After this, you'll see that the identity has two identifiers but if you try to log in with any of them, you'll get the error I shared.
If you remove old identifier from DB, you'll see you are able to login with the new account and that the identity is the same.
Relevant log output
{"audience":"application","error":{"debug":"Unable to find credentials that match the given provider \"google\" and subject \"9876\".","message":"An internal server error occurred, please contact the system administrator","reason":"Unable to find matching OpenID Connect Credentials.","stack_trace":"\ngithub.com/ory/kratos/selfservice/strategy/oidc.(*Strategy).processLogin\n\t/project/selfservice/strategy/oidc/strategy_login.go:180\ngithub.com/ory/kratos/selfservice/strategy/oidc.(*Strategy).HandleCallback\n\t/project/selfservice/strategy/oidc/strategy.go:434\ngithub.com/ory/kratos/selfservice/strategy.disabledWriter\n\t/project/selfservice/strategy/handler.go:28\ngithub.com/ory/kratos/selfservice/strategy/oidc.(*Strategy).setRoutes.IsDisabled.func1\n\t/project/selfservice/strategy/handler.go:33\ngithub.com/ory/kratos/x.(*RouterPublic).GET.NoCacheHandle.func1\n\t/project/x/nocache.go:21\ngithub.com/ory/kratos/x.(*RouterPublic).Handle.NoCacheHandle.func1\n\t/project/x/nocache.go:21\ngithub.com/julienschmidt/httprouter.(*Router).ServeHTTP\n\t/go/pkg/mod/github.com/julienschmidt/httprouter@v1.3.0/router.go:387\ngithub.com/ory/nosurf.(*CSRFHandler).handleSuccess\n\t/go/pkg/mod/github.com/ory/nosurf@v1.2.7/handler.go:234\ngithub.com/ory/nosurf.(*CSRFHandler).ServeHTTP\n\t/go/pkg/mod/github.com/ory/nosurf@v1.2.7/handler.go:191\ngithub.com/urfave/negroni.(*Negroni).UseHandler.Wrap.func1\n\t/go/pkg/mod/github.com/urfave/negroni@v1.0.0/negroni.go:46\ngithub.com/urfave/negroni.HandlerFunc.ServeHTTP\n\t/go/pkg/mod/github.com/urfave/negroni@v1.0.0/negroni.go:29\ngithub.com/urfave/negroni.middleware.ServeHTTP\n\t/go/pkg/mod/github.com/urfave/negroni@v1.0.0/negroni.go:38\ngithub.com/ory/kratos/x.glob..func1\n\t/project/x/clean_url.go:15\ngithub.com/urfave/negroni.HandlerFunc.ServeHTTP\n\t/go/pkg/mod/github.com/urfave/negroni@v1.0.0/negroni.go:29\ngithub.com/urfave/negroni.middleware.ServeHTTP\n\t/go/pkg/mod/github.com/urfave/negroni@v1.0.0/negroni.go:38\ngithub.com/rs/cors.(*Cors).ServeHTTP\n\t/go/pkg/mod/github.com/rs/cors@v1.8.2/cors.go:266\ngithub.com/ory/kratos/cmd/daemon.servePublic.func1\n\t/project/cmd/daemon/serve.go:114\ngithub.com/urfave/negroni.HandlerFunc.ServeHTTP\n\t/go/pkg/mod/github.com/urfave/negroni@v1.0.0/negroni.go:29\ngithub.com/urfave/negroni.middleware.ServeHTTP\n\t/go/pkg/mod/github.com/urfave/negroni@v1.0.0/negroni.go:38\nnet/http.HandlerFunc.ServeHTTP\n\t/usr/local/go/src/net/http/server.go:2136\ngithub.com/prometheus/client_golang/prometheus/promhttp.InstrumentHandlerResponseSize.func1\n\t/go/pkg/mod/github.com/prometheus/client_golang@v1.13.0/prometheus/promhttp/instrument_server.go:284\nnet/http.HandlerFunc.ServeHTTP\n\t/usr/local/go/src/net/http/server.go:2136\ngithub.com/prometheus/client_golang/prometheus/promhttp.InstrumentHandlerCounter.func1\n\t/go/pkg/mod/github.com/prometheus/client_golang@v1.13.0/prometheus/promhttp/instrument_server.go:142\nnet/http.HandlerFunc.ServeHTTP\n\t/usr/local/go/src/net/http/server.go:2136\ngithub.com/prometheus/client_golang/prometheus/promhttp.InstrumentHandlerDuration.func1\n\t/go/pkg/mod/github.com/prometheus/client_golang@v1.13.0/prometheus/promhttp/instrument_server.go:92\nnet/http.HandlerFunc.ServeHTTP\n\t/usr/local/go/src/net/http/server.go:2136\ngithub.com/prometheus/client_golang/prometheus/promhttp.InstrumentHandlerDuration.func2\n\t/go/pkg/mod/github.com/prometheus/client_golang@v1.13.0/prometheus/promhttp/instrument_server.go:104\nnet/http.HandlerFunc.ServeHTTP\n\t/usr/local/go/src/net/http/server.go:2136\ngithub.com/prometheus/client_golang/prometheus/promhttp.InstrumentHandlerRequestSize.func1\n\t/go/pkg/mod/github.com/prometheus/client_golang@v1.13.0/prometheus/promhttp/instrument_server.go:234\nnet/http.HandlerFunc.ServeHTTP\n\t/usr/local/go/src/net/http/server.go:2136\ngithub.com/ory/x/prometheusx.Metrics.Instrument.Metrics.instrumentHandlerStatusBucket.func1\n\t/go/pkg/mod/github.com/ory/x@v0.0.614/prometheusx/metrics.go:115\nnet/http.HandlerFunc.ServeHTTP\n\t/usr/local/go/src/net/http/server.go:2136","status":"Internal Server Error","status_code":500},"file":"/go/pkg/mod/github.com/ory/x@v0.0.614/logrusx/helper.go:125","func":"github.com/ory/x/logrusx.(*Logger).Logf","http_request":{"headers":{"accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7","accept-encoding":"gzip, deflate, br, zstd","accept-language":"en-GB,en-US;q=0.9,en;q=0.8","connection":"keep-alive","cookie":"Value is sensitive and has been redacted. To see the value set config key \"log.leak_sensitive_values = true\" or environment variable \"LOG_LEAK_SENSITIVE_VALUES=true\".","sec-ch-ua":"\"Chromium\";v=\"124\", \"Google Chrome\";v=\"124\", \"Not-A.Brand\";v=\"99\"","sec-ch-ua-mobile":"?0","sec-ch-ua-platform":"\"macOS\"","sec-fetch-dest":"document","sec-fetch-mode":"navigate","sec-fetch-site":"cross-site","sec-fetch-user":"?1","upgrade-insecure-requests":"1","user-agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"},"host":"localhost:4433","method":"GET","path":"/self-service/methods/oidc/callback/google","query":"Value is sensitive and has been redacted. To see the value set config key \"log.leak_sensitive_values = true\" or environment variable \"LOG_LEAK_SENSITIVE_VALUES=true\".","remote":"192.168.65.1:19797","scheme":"http"},"level":"error","msg":"An error occurred and is being forwarded to the error user interface.","otel":{"span_id":"730ea6593b74d6da","trace_id":"6ac5e57760212891d1deb5a13ef4c024"},"service_name":"Ory Kratos","service_version":"v1.1.0","time":"2024-04-24T08:00:14.443172595Z"}
Relevant configuration
No response
Version
v1.1.0
On which operating system are you observing this issue?
Linux
In which environment are you deploying?
Docker Compose
Additional Context
No response