ory / kratos

The most scalable and customizable identity server on the market. Replace your Homegrown, Auth0, Okta, Firebase with better UX and DX. Has all the tablestakes: Passkeys, Social Sign In, Multi-Factor Auth, SMS, SAML, TOTP, and more. Written in Go, cloud native, headless, API-first. Available as a service on Ory Network and for self-hosters.

Home Page:https://www.ory.sh/?utm_source=github&utm_medium=banner&utm_campaign=kratos

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Multiple OIDC identifiers for an identity don't work

noells opened this issue · comments

Preflight checklist

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:

  1. 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.
  2. 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:

  1. Log in with one OIDC account. Access the Kratos identity and save the credential identifier
  2. Delete account from step 1
  3. Log in with a different OIDC account from step 1 but for the same OIDC provider
  4. 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
  1. 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