Skip to content

Commit

Permalink
[feature] overhaul the oidc system (#961)
Browse files Browse the repository at this point in the history
* [feature] overhaul the oidc system

this allows for more flexible username handling and prevents account
takeover using old email addresses

* [feature] add migration path for old OIDC users

* [feature] nicer error reporting for users

* [docs] document the new OIDC flow

* [fix] return early on oidc error

* [docs]: add comments on the finalization logic
  • Loading branch information
theSuess authored Dec 6, 2022
1 parent 1a3f26f commit 199b685
Show file tree
Hide file tree
Showing 20 changed files with 333 additions and 117 deletions.
2 changes: 1 addition & 1 deletion cmd/gotosocial/action/admin/account/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ var Create action.GTSAction = func(ctx context.Context) error {
return err
}

_, err = dbConn.NewSignup(ctx, username, "", false, email, password, nil, "", "", true, false)
_, err = dbConn.NewSignup(ctx, username, "", false, email, password, nil, "", "", true, "", false)
if err != nil {
return err
}
Expand Down
59 changes: 27 additions & 32 deletions docs/configuration/oidc.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ oidc-scopes:
- "email"
- "profile"
- "groups"

# Bool. Link OIDC authenticated users to existing ones based on their email address.
# This is mostly intended for migration purposes if you were running previous versions of GTS
# which only correlated users with their email address. Should be set to false for most usecases.
# Options: [true, false]
# Default: false
oidc-link-existing: false
```
## Behavior
Expand All @@ -76,48 +83,36 @@ When OIDC is enabled on GoToSocial, the default sign-in page redirects automatic
This means that OIDC essentially *replaces* the normal GtS email/password sign-in flow.
When a user logs in through OIDC, GoToSocial will request that user's preferred email address and username from the OIDC provider. It will then use the returned email address to either:
*If the email address is already associated with a user/account*: sign the requester in as that user/account.
Or:
*If the email address is not yet associated with a user/account*: create a new user and account with the returned credentials, and sign the requester in as that user/account.
In other words, GoToSocial completely delegates sign-in authority to the OIDC provider, and trusts whatever credentials it returns.
### Username conflicts
In some cases, such as when a server has been switched to use OIDC after already using default settings for a while, there may be an overlap between usernames returned from OIDC, and usernames that already existed in the database.
For example, let's say that someone with username `gordonbrownfan` and email address `gordon_is_best@example.org` has an account on a GtS instance that uses the default sign-in flow.
Due to the way the ActivityPub standard works, you _cannot_ change your username
after it has been set. This conflicts with the OIDC spec which does not
guarantee that the `preferred_username` field is stable.

That GtS instance then switches to using OIDC login. However, in the OIDC's storage there's also a user with username `gordonbrownfan`. If this user has the email address `gordon_is_best@example.org`, then GoToSocial will assume that the two users are the same and just log `gordonbrownfan` in as though nothing had changed. No problem!
To work with this, we ask the user to provide a username on their first login
attempt. The field for this is pre-filled with the value of the `preferred_username` claim.

However, if the user in the OIDC storage has a different email address, GoToSocial will try to create a new user and account for this person.
After authenticating, GtS stores the `sub` claim supplied by the OIDC provider.
On subsequent authentication attempts, the user is looked up using this claim
exclusively.

Since the username `gordonbrownfan` is already taken, GoToSocial will try `gordonbrownfan1`. If this is also taken, it will try `gordonbrownfan2`, and so on, until it finds a username that's not yet taken. It will then sign the requester in as that user/account, distinct from the original `gordonbrownfan`.

### Malformed usernames

A username returned from an OIDC provider might not always fit the pattern of what GoToSocial accepts as a valid username, ie., lower-case letters, numbers, and underscores. In this case, GoToSocial will do its best to parse the returned username into something that fits the pattern.

For example, say that an OIDC provider returns the username `Marx Is Great` for a sign in, which doesn't fit the pattern because it contains upper-case letters and spaces.

In this case, GtS will convert it into `marx_is_great` by applying the following rules:

1. Trim any leading or trailing whitespace.
2. Convert all letters to lowercase.
3. Replace spaces with underscores.

Unfortunately, at this point GoToSocial doesn't know how to handle returned usernames containing special characters such as `@` or `%`, so these will return an error.
This then allows you to change the username on a provider level without losing
access to your GtS account.

### Group membership

Most OIDC providers allow for the concept of groups and group memberships in returned claims. GoToSocial can use group membership to determine whether or not a user returned from an OIDC flow should be created as an admin account or not.

If the returned OIDC groups information for a user contains membership of the groups `admin` or `admins`, then that user will be created/signed in as though they are an admin.

## Migrating from old versions

If you're moving from an old version of GtS which used the unstable `email`
claim for unique user identification, you can set the `oidc-link-existing`
configuration to `true`. If no user can be found for the ID returned by the
provider, a lookup based on the `email` claim is performed instead. If this
succeeds, the stable id is added to the database for the matching user.

You should only use this for a limited time to avoid malicious account takeover.

## Provider Examples

### Dex
Expand Down
7 changes: 7 additions & 0 deletions example/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,13 @@ oidc-scopes:
- "profile"
- "groups"

# Bool. Link OIDC authenticated users to existing ones based on their email address.
# This is mostly intended for migration purposes if you were running previous versions of GTS
# which only correlated users with their email address. Should be set to false for most usecases.
# Options: [true, false]
# Default: false
oidc-link-existing: false

#######################
##### SMTP CONFIG #####
#######################
Expand Down
6 changes: 6 additions & 0 deletions internal/api/client/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ const (
// OauthAuthorizePath is the API path for authorization requests (eg., authorize this app to act on my behalf as a user)
OauthAuthorizePath = "/oauth/authorize"

// OauthFinalizePath is the API path for completing user registration with additional user details
OauthFinalizePath = "/oauth/finalize"

// CallbackPath is the API path for receiving callback tokens from external OIDC providers
CallbackPath = oidc.CallbackPath

Expand All @@ -64,6 +67,8 @@ const (
sessionScope = "scope"
sessionInternalState = "internal_state"
sessionClientState = "client_state"
sessionClaims = "claims"
sessionAppID = "app_id"
)

// Module implements the ClientAPIModule interface for
Expand Down Expand Up @@ -93,6 +98,7 @@ func (m *Module) Route(s router.Router) error {
s.AttachHandler(http.MethodPost, OauthAuthorizePath, m.AuthorizePOSTHandler)

s.AttachHandler(http.MethodGet, CallbackPath, m.CallbackGETHandler)
s.AttachHandler(http.MethodPost, OauthFinalizePath, m.FinalizePOSTHandler)

s.AttachHandler(http.MethodGet, oauth.OOBTokenPath, m.OobHandler)
return nil
Expand Down
Loading

0 comments on commit 199b685

Please sign in to comment.