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.
https://www.ory.sh/?utm_source=github&utm_medium=banner&utm_campaign=kratos
Apache License 2.0
11.32k stars 963 forks source link

User invites flow #2525

Open splaunov opened 2 years ago

splaunov commented 2 years ago

Preflight checklist

Context and scope

User invites are common in many systems. It's when administrator sends an inviting message (email or sms) to a potential user. User clicks a link in the message and starts sign up flow. It is different from simple user registration (sign up) in that administrator could set some data for future user in the system. E.g. add the user to some group/team, assign certain permissions/role. So, when the user first logins he/she has everything set up and ready for the user's role.

As of today, kratos provides account recovery flow to support this case. This approach was discussed in here: https://github.com/ory/kratos/discussions/1342 https://github.com/ory/kratos/issues/595 https://github.com/ory/kratos/issues/2473

While this solves the problem at least for email/password type of login, but have some drawbacks.

  1. An identity should not be created in the system before user accepted terms of service. But this recovery-based solution requires creating identity before user is invited.
  2. Created identity should eventually be deleted if user has not logged in. Need some job to clean up such stale identities.
  3. Some systems may require different UI for pure recovery and invite flow. In this regard invite flow should be similar to new user registration ("you successfully registered, please download our app"). As for now, it is not possible to differentiate these two different cases in recovery flow.
  4. Integrated system should manage inviting messages sending and templating. While kratos' built in courier could be used for this.
  5. If user is invited by phone and is expected to login with SMS code, the recovery workaround won't work. A link to login flow should be sent in invite instead of a recovery link.

Goals and non-goals

The goal is to move user invites management to kratos. This simplifies integration and improves end user experience.

The design

Create new db table to track user invites. One of the table fields should store invite's context data in json format. This data should be managed by integrated system.

APIs

No response

Data storage

No response

Code and pseudo-code

No response

Degree of constraint

No response

Alternatives considered

No response

aeneasr commented 2 years ago

Thank you for the detailed description! We recently implemented in invite system in Ory Cloud. From that experience I can say that there are many different ways to approach user invites and it all depends on the flows you want in your user experience, balanced with security. There are so many questions (this is just an excerpt):

  1. Who can invite
  2. Who can be invited
  3. Can the inviter see if the invitee exists
  4. How can the invite be accepted
  5. What happens if the user exists and the invite is accepted
  6. What happens if the user doesn't exists and the invite is accepted
  7. Do you need to prevent account enumeration?
  8. ...

I think this topic is too complex to solve generically and I think it is out of scope of what Ory Kratos tries to deliver. We can probably improve some of the APIs around this (e.g. sending an email with an account set up link), but I don't think that we can add functionality that answers these difficult questions above easily.

woylie commented 2 years ago

I recently implemented an invite flow around the Kratos API. It works fine, the one awkward thing is that the email address needs to be verified after the invite link (recovery link) is opened (#2473).

1. Who can invite
2. Who can be invited
3. Can the inviter see if the invitee exists

These are all points that Kratos cannot make any assumptions about. It is pretty clear that this flow would need to be initiated via the admin API, so that the client application can handle all of these details.

4. How can the invite be accepted
5. What happens if the user exists and the invite is accepted
6. What happens if the user doesn't exists and the invite is accepted

Here I'd say Kratos could very well make assumptions: When the flow is initialized, a link is generated, that link is sent to the user, and when the link is opened, a registration form is rendered that allows the user to set up credentials and set the traits. The form values can be initialized with data submitted when initializing the invite flow. Since the registration is tied to the invite, there should be no form field for the identifier. During the flow initializing, it would be possible to set values that cannot be set during the self-service flows (meta data, schema ID).

7. Do you need to prevent account enumeration?

It depends, indeed.

I think this topic is too complex to solve generically and I think it is out of scope of what Ory Kratos tries to deliver. We can probably improve some of the APIs around this (e.g. sending an email with an account set up link), but I don't think that we can add functionality that answers these difficult questions above easily.

So in summary, if Kratos does not want to support invite flows natively, the one missing piece in my opinion is something like:

POST /admin/verification
identity_id: {id}
identifier: {id}

But still, I would see a flow like this very fitting for Kratos:

  1. POST /admin/invite Initializes invite flow, generates invite link and delivers it to the given identifier. Parameters: schema_id, identifier, identifier_field (pointing to email or tel trait), traits, metadata_public, metadata_private, expires_in

  2. GET /self-service/invite?flow={flow_id}&token={token} Render registration form without the identifier trait used in the invite and with the traits of the schema that was specified when the flow was initialized. Trait fields are initialized with the values from the invite initialization.

  3. POST /self-service/invite?flow={flow_id} Submit invitation flow. Create user traits form form data and identifier, metadata_public, metadata_private and schema ID from the invite.

I could make a more detailed sequence diagram, if that would help. Unless you say that this is definitely out of scope.

(additionally, CRUD admin routes for those invites would be helpful)

aeneasr commented 2 years ago

Thank you for the detailed reply!

Since the registration is tied to the invite, there should be no form field for the identifier.

I think this serves a bit my point, this is not always the case. Sometimes you do care that the user who signs up is using the email you invited, but sometimes you do not care about that. It depends on the use case! That makes this thing very difficult to implement in a generic way, IMO.

init invite flow, identifier in use: return conflict error - the flow is initialized via the admin API, the client application can deal with the error however it sees fit

The flow we went for is that the user receives two types of email:

  1. User exists, accept invite
  2. User does not exist, send to registration

So here too, this depends highly on the use case and desired user flow. One team might decide for our approach, another team for another. Making this configurable and usable without being opinionated will be a lot of work and will, IMO, result in a difficult to use product.

POST /admin/verification
identity_id: {id}
identifier: {id}

That is on the backlog and definitely possible at some point!

woylie commented 2 years ago

Since the registration is tied to the invite, there should be no form field for the identifier.

I think this serves a bit my point, this is not always the case. Sometimes you do care that the user who signs up is using the email you invited, but sometimes you do not care about that. It depends on the use case! That makes this thing very difficult to implement in a generic way, IMO.

I think an invite flow in the scope of Kratos only makes sense if you want to allow users to register with properties that are normally not available in the public registration form (like a different schema ID or metadata), or if the self-service registration is completely disabled. Otherwise you can just send a link directly to the registration form (you might want to add a query parameter to keep track of who invited whom, but that is of no concern for Kratos).

If you really want to allow anyone who gets hold of the link to register with those privileged properties, I guess this could be accomplished without too much overhead by making the identifier parameter on POST /admin/invite optional and just returning the generated link instead of delivering it. If the identifier is set: don't show field. If identifier is not set: show field, allow any value.

init invite flow, identifier in use: return conflict error - the flow is initialized via the admin API, the client application can deal with the error however it sees fit

The flow we went for is that the user receives two types of email:

1. User exists, accept invite

2. User does not exist, send to registration

So here too, this depends highly on the use case and desired user flow. One team might decide for our approach, another team for another. Making this configurable and usable without being opinionated will be a lot of work and will, IMO, result in a difficult to use product.

This is more difficult, indeed. I'm just thinking along the lines that Kratos wouldn't need to support any kind of invite scenarios, since not all invite scenarios deal with privileged access or privileged user properties, or they deal with properties or relationships that are handled in an external database anyway anyway. So the use cases in which Kratos can be helpful might actually be narrower than expected. I don't know.

POST /admin/verification
identity_id: {id}
identifier: {id}

That is on the backlog and definitely possible at some point!

👍

splaunov commented 2 years ago

Have added this to the issue description:

  1. If user is invited by phone and is expected to login with SMS code, the recovery workaround won't work. A link to login flow should be sent in invite instead of a recovery link.

In case we don't add new "invite" flow to kratos, we definitely need to use registration flow for invited users. And it would be possible even today without any extensions, but the key question is how to link the newly registered user to the invite context in the integrated system.

aeneasr commented 2 years ago

You can probably use the metadata_admin field for that!

splaunov commented 2 years ago

Yes, didn't know it exists. Can we add parameters to self-service/registration/browser? So, metadata_admin could be preset on registration flow init.

aeneasr commented 2 years ago

Can we add parameters to self-service/registration/browser? So, metadata_admin could be preset on registration flow init.

Nope, because that endpoint is accessible to anyone :/

splaunov commented 2 years ago

What could be a solution then? We need to set invite id/token to registration flow somehow. Otherwise registration flow cannot be used for invites.

aeneasr commented 2 years ago

Web hooks! In particular: https://github.com/ory/kratos/pull/1585

splaunov commented 2 years ago

Blocking web hooks will make all registrations a bit slower, even not invited. But should work if we use email/phone as invite identifier and user won't change email/phone on registration.

We can also use email/phone as invite identifier without involving web hooks. Just search invite by phone/email when user authenticates in the integrated app and link the identity to the invite.

Can we at least add parameters to self-service/registration/browser to preset email/phone traits? Otherwise user will need to enter it manually and can accidentally use other email or phone than the invite was sent to.

splaunov commented 2 years ago

@aeneasr what do you think about this?

Can we at least add parameters to self-service/registration/browser to preset email/phone traits?

As a workaround, it's possible to set cookie on self-service ui request selfservice-ui/auth/registration?email=...

and then use and delete the cookie after redirect on

selfservice-ui/auth/registration?flow=...

But this method means that the preset value will be lost if user chooses to refresh the page for whatever reason. It would be much better to preset values directly to the flow ui data on kratos side.

aeneasr commented 2 years ago

Can we at least add parameters to self-service/registration/browser to preset email/phone traits?

That's a pretty cool idea!

woylie commented 2 years ago

Can we at least add parameters to self-service/registration/browser to preset email/phone traits?

That's a pretty cool idea!

I wonder whether that might be useful for other flows as well? I wanted to pre-fill the email field of the verification form recently, and I did that by adding a query parameter when initializing the flow and reading it from the request_url field of the flow. Would make sense to me if Kratos initializes the field from a query parameter there as well.

github-actions[bot] commented 1 year ago

Hello contributors!

I am marking this issue as stale as it has not received any engagement from the community or maintainers for a year. That does not imply that the issue has no merit! If you feel strongly about this issue

Throughout its lifetime, Ory has received over 10.000 issues and PRs. To sustain that growth, we need to prioritize and focus on issues that are important to the community. A good indication of importance, and thus priority, is activity on a topic.

Unfortunately, burnout has become a topic of concern amongst open-source projects.

It can lead to severe personal and health issues as well as opening catastrophic attack vectors.

The motivation for this automation is to help prioritize issues in the backlog and not ignore, reject, or belittle anyone.

If this issue was marked as stale erroneously you can exempt it by adding the backlog label, assigning someone, or setting a milestone for it.

Thank you for your understanding and to anyone who participated in the conversation! And as written above, please do participate in the conversation if this topic is important to you!

Thank you 🙏✌️

mitar commented 1 year ago

I think this is still relevant.