dotnet / dotnet/aspnetcore

In ASP.NET Core Identity, the IdentityUserToken class should include a "LoginProviderKey" property

Open
#29,569 2 comments 0 reactions 0 assignees View on GitHub
affected-few area-identity breaking-change enhancement severity-major
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 5h
Merged PRs (30d)
276

Description

## Summary

A ```LoginProviderKey``` property (and corresponding DB column with EF Core) should be added to ```Microsoft.AspNetCore.Identity.IdentityUserToken``` so that the token store can properly store multiple sets of tokens from multiple external user accounts from the same external provider. This will make it inline with the design behavior of ```IdentityUserLogin```.

## Motivation and goals

While working on a project that allows a user to link multiple external accounts from multiple external identity providers, I discovered a discrepancy between the ```IdentityUserLogin``` class (underlying DB table ```AspNetUserLogins```) and ```IdentityUserToken``` (underlying DB table ```AspNetUserTokens```).

The ```IdentityUserLogin``` class has the ```LoginProvider``` and ```ProviderKey``` properties as a composite key. Because of this, one local user is allowed to have multiple external logins from the same provider associated, because ```ProviderKey``` can be used to store the user's unique object ID at the provider. For example, a user is allowed to have 2 Google accounts as external logins, despite both having the same ```LoginProvider``` "Google".

Now some applications need to store user's tokens obtained from these external providers, that's why ASP.NET Core Identity provided the ```IdentityUserToken``` class as well as methods such as ```_signInManager.UpdateExternalAuthenticationTokensAsync``` that makes storing and accessing such tokens easy.

However, ```IdentityUserToken``` and its DB datastore only has the ```LoginProvider``` property, without a ```LoginProviderKey```. This means this token store can only manage one set of keys for one provider. If a user has multiple "Google" accounts, the store won't work using its intended structure.

The fundamental issue here is that associating a "provider" (as in the name of the provider) with a set of tokens does not make sense. Tokens are issued for individual user accounts at the provider, not the provider itself.

In other words, if a single property/column had to be used, it would have been better if it used ```LoginProviderKey``` instead of ```LoginProvider``` for this table. Combined with the ```UserId``` foreign key, we can navigate back to the ```AspNetUserLogins``` table to find the ```LoginProvider``` anyway (it is almost impossible for a given user to have duplicate ```LoginProviderKey``` ).

To work around this issue, one can

1. Extend ```IdentityUserToken``` by inheriting it and adding the extra property. But doing so makes built-in token related helper methods less useful.

2. Re-arrange the storing of data into the existing properties. For example, one can choose to store ```LoginProviderKey``` inside the ```LoginProvider``` column, and use ```LoginProviderKey``` to store the actual provider name (essentially swapping these 2 properties for their intended use). But this makes the naming confusing and non-intuitive. It may also break some other built-in methods.

I feel neither way is ideal. Since ```IdentityUserLogin``` was already designed to be multi-provider and multi-userlogin friendly, it makes sense that ```IdentityUserToken``` should follow suit.

## Risks / unknowns

Currently the AspNetUserTokens table has ```UserId```, ```LoginProvider``` and ```Name``` together as a composite key. Adding ```LoginProviderKey``` should change this design such that ```LoginProvider``` , ```LoginProviderKey``` and ```Name``` should be used as the composite key, following the pattern of ```IdentityUserLogin```. However, such change can break existing implementations.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.