Skip to content

[Blazor][Wasm] Adds a library for performing autentication in blazor webassembly applications using OIDC #18851

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 17, 2020

Conversation

javiercn
Copy link
Member

@javiercn javiercn commented Feb 6, 2020

This library includes the following components:

  • AuthenticationManager: Is a component responsible for handling all the authentication operations.
  • RemoteAuthenticationService: Handles the authentication details of the underlying protocol and provides information about the current user.
  • IAccessTokenProvider: Handles the acquisition of access tokens used to authorize against resources by the app.

The general expected flow to follow by the templates is as follows:

  • All authentication operations are handled by the AuthenticationManager inside a page that by default maps to
    '/authentication/{action}'.
  • When a user is not authorized, it gets redirected to '/authentication/login' which sets up the appropriate state and redirects the user if necessary to the identity provider or successfully returns the user back to the authentication page if it was able to authenticate the user without requiring a redirect;
  • When a redirection is needed, the user is sent bck to /authentication/login-callback where the authentication process is either completed successfully (and the user is redirected back to where he was originally) or the user is sent to /authentication/login-failed to indicate the reason of the authentication failure.

The authentication manager accepts multiple parameters to configure the UI that is displayed at each stage of the authentication process. Those steps are:

  • Login
  • Login callback
  • Login failed
  • Logout
  • Logout callback
  • Logout failed
  • Logout succeeded

Authenticated users can request access tokens by injecting the IAccessTokenProvider service into their app and calling GetAccessToken().

By default, a token with the default scopes is provisioned, but additional tokens might be provisioned by passing options to the GetAccessToken method.

This method returns an AccessTokenResult object that indicates whether a token is available or a redirect operation is needed to acquire the token.

In that case, the user can preserve the state locally and navigate to the url in the result to start the token provisioning process, after which, it will be sent back to the current point.

There are a few more tests that I'm adding and low level implementation details that I'm handling, but it is ready for review.

@javiercn javiercn removed the request for review from dougbu February 6, 2020 12:20
@Pilchie Pilchie added the area-blazor Includes: Blazor, Razor Components label Feb 6, 2020
@javiercn javiercn force-pushed the javiercn/blazor-wasm-oidc-core-lib branch 6 times, most recently from e2df786 to 88e365f Compare February 9, 2020 17:54
@SteveSandersonMS SteveSandersonMS self-requested a review February 10, 2020 15:44
Copy link
Member

@SteveSandersonMS SteveSandersonMS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, this PR is epic!

Even though I have posted literally one million comments, I want to emphasise that I am totally in favour of this happening, and in general am really happy with the overall approach you're taking. I strongly support this work being done and am glad you're doing it!

@SteveSandersonMS
Copy link
Member

Also in case it got lost in the flood of comments above, I want to ask again for any further work on this PR to be added as regular extra commits (non-squashed, non-force-pushed) so that follow-up review can just cover the subsequent diffs. It's taken about 4 hours to read through the first time 😄

Copy link
Member Author

@javiercn javiercn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've addressed things in individual commits. There are still things to address here that are listed here:

I will go through the comments later today/tomorrow and resolve/answer comments as appropriate.

Copy link
Member Author

@javiercn javiercn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the PR to address all the feedback, the only thing I haven't been able to do is the Enum switch as it breaks for some reason I can't tell (with a massive call stack), so I leave that as a follow-up.

/// </summary>
[Parameter] public RenderFragment LoginFragment { get; set; } = DefaultLoginFragment;
[Parameter] public RenderFragment LogingIn { get; set; } = DefaultLogInFragment;
Copy link
Member

@SteveSandersonMS SteveSandersonMS Feb 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this is also fixed in a later commit, but just in case it isn't:

Suggested change
[Parameter] public RenderFragment LogingIn { get; set; } = DefaultLogInFragment;
[Parameter] public RenderFragment LoggingIn { get; set; } = DefaultLogInFragment;

/// </summary>
[Parameter] public RenderFragment LoginCallbackFragment { get; set; } = DefaultLoginCallbackFragment;
[Parameter] public RenderFragment CompletingLogingIn { get; set; } = DefaultLogInCallbackFragment;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[Parameter] public RenderFragment CompletingLogingIn { get; set; } = DefaultLogInCallbackFragment;
[Parameter] public RenderFragment CompletingLoggingIn { get; set; } = DefaultLogInCallbackFragment;

/// </summary>
[Parameter] public RenderFragment LoggedOutFragment { get; set; } = DefaultLoggedOutFragment;
[Parameter] public RenderFragment LogOutSucceded { get; set; } = DefaultLoggedOutFragment;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[Parameter] public RenderFragment LogOutSucceded { get; set; } = DefaultLoggedOutFragment;
[Parameter] public RenderFragment LogOutSucceeded { get; set; } = DefaultLoggedOutFragment;

/// An <see cref="RemoteAuthenticatorViewCore{TAuthenticationState}"/> that uses <see cref="RemoteAuthenticationState"/> as the
/// state to be persisted across authentication operations.
/// </summary>
public class RemoteAuthenticatorView : RemoteAuthenticatorViewCore<RemoteAuthenticationState>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking really good now.

One thing I'd like to check to complete my understanding: do we already have use cases for needing to inherit directly from RemoteAuthenticatorViewCore<T> elsewhere (e.g., for msal or something)? If we do then great, this all makes sense. If we don't yet have reasons to do that and the base class only exists for future extensibility, we should consider making the base class internal for now.

@@ -119,8 +125,14 @@ public virtual async ValueTask<AccessTokenResult> GetAccessToken(AccessTokenRequ
}

/// <inheritdoc />
public virtual async Task<ClaimsPrincipal> GetCurrentUser()
protected virtual async ValueTask<ClaimsPrincipal> GetCurrentUser()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the 1-minute caching you've put in here. That feels like a good balance of efficiency and freshness.

Just so I can explain this to other people, can you summarise what it is that happens each time the 1-minute cache expires? What will we say in docs? Something like: The next time we evaluate authentication status, the system will make an HTTP request to the remote authentication service to verify that the user is still authenticated, and to update the list of claims associated with their ClaimsPrincipal. Is that accurate?

@@ -120,8 +127,24 @@ public virtual async ValueTask<AccessTokenResult> GetAccessToken()
/// <inheritdoc />
public virtual async ValueTask<AccessTokenResult> GetAccessToken(AccessTokenRequestOptions options)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have some naming issues here now. If I'm reading this correctly, the usage pattern would look like:

var accessTokenResult = await service.GetAccessToken(options);
if (accessTokenResult.TryGetAccessToken(out var accessToken))
{
    // ...
}

Issues:

  1. GetAccessToken should have an Async suffix on its name, right?
  2. It's confusing that both of the methods are basically called "get access token". If you got it the first time, why do you have to get it again?

I wish C# had a richer type system for this. The ideal result type would be AccessToken | AccessTokenRedirection. But since that's not an option, I'm tending to prefer the low-ceremony option you originally had, e.g.,

var result = await service.GetAccessTokenAsync(options);
if (result.Success)
{
    // ... use result.Token
}
else
{
    // ... use result.RedirectionUrl
}

If we're feeling fancy, we might even consider defining a deconstructor on AccessTokenResult so that in all the docs, we'd demonstrate its consumption like this:

var (token, redirectionUrl) = await service.GetAccessTokenAsync(options);
if (token != null)
{
    // ... use token
}
else
{
    // ... use redirectionUrl
}

However this relies on the idea that there will never be any other status besides "got token" and "needs redirection".

Let's discuss this later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could even rely on C#8 nullables to communicate the intent more clearly. If accessTokenResult.Token and accessTokenResult.RedirectionUrl were both marked as nullable, the compiler will force people to at least think about the possibility that the token isn't provided.

@javiercn javiercn force-pushed the javiercn/blazor-wasm-oidc-core-lib branch from be96bfd to 7855aef Compare February 14, 2020 13:50
@SteveSandersonMS SteveSandersonMS self-requested a review February 17, 2020 09:14
Copy link
Member

@SteveSandersonMS SteveSandersonMS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fantastic - great job @javiercn! Thanks for going through it with me in so much detail.

There are still a few typos in APIs to be resolved before merging - please see my suggested fixes (e.g., this one). I think all the typos are in members whose names contain LogingIn or Succeded.

Then, :shipit: !

* Adds a Microsoft.AspNetCore.Components.WebAssembly.Authentication
  library for performing authentication in Blazor webassembly.
* Includes a default implementation that supports OIDC capable IdPs
  using oidc-client.js
* Includes multiple primitives to deal with authentication flows and
  supports acquiring access tokens to call APIs.
  * RemoteAuthenticatorView is responsible for handling authentication
    operations at the user interface level.
  * RemoteAuthenticatorService is responsible for handling the lower
    level authentication details by using JavaScript interop to interact
    with the underlying javascript library implementing the auth protocol.
  * SignOutSessionStateManager handles CSRF protection for the logout
    path.
  * IAccessTokenProvider handles provisioning access tokens to call APIs.
@javiercn javiercn force-pushed the javiercn/blazor-wasm-oidc-core-lib branch from 30d3143 to 28c3bc1 Compare February 17, 2020 22:01
@javiercn javiercn merged commit 0dbb01b into blazor-wasm Feb 17, 2020
@dougbu dougbu deleted the javiercn/blazor-wasm-oidc-core-lib branch May 18, 2020 19:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-blazor Includes: Blazor, Razor Components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants