简体   繁体   中英

ASP.NET Core authentication/authorization with external OpenID Connect provider

I'm learning authentication/authorization in ASP.NET Core and got confused by which components I should use in my scenario: I have an SPA frontend and an ASP.NET Core API backend. I'm using a third-party OpenID Connect provider (like Okta) that only supports Authorization Code Flow, so I believe only the backend should talk to this external provider. The external provider only handles authentication, not authorization. Our application needs role-based access control.

  • Do we need Identity Server, considering authentication is already handled by the external OpenID Connect provider?
  • Do we need ASP.NET Core Identity?
  • How should we approach authorization in this scenario?

(I assume by "Identity Server" you're referring to IdentityServer4 or maybe even IdentityServer3)

Do we need Identity Server, considering authentication is already handled by the external OpenID Connect provider?

No.

People use IdentityServer4 when they want to self-host their own IdP (and of course, you can still federate with Google, Facebook, Apple, etc when you self-host your own IdP too), but when you use an IdP-as-a-service like Okta or Azure AD then you don't need to run your own IdP.

Do we need ASP.NET Core Identity?

No.

ASP.NET Core Identity is basically a utility-kit and time-saving library that (ostensibly...) makes it easier to crank-out the usual user-management features every traditional web-application needs: things like Registration, Password Resets, Automatic Lockout, Email+Phone verification, are all handled for you by ASP.NET Core Identity - including the user database via Entity Framework.

...but when you use an IdP-as-a-service then those same features (Registration, Forgot-password, etc) are supposed to be part of the service you're paying for (so you could either spend tens of thousands of dollars of dev-time building a self-hosted IdentityServer4 IdP - or you could skip all that and pay Okta merely tens of dollars per month instead. Guess which option your project budget manager wants to go with...

With ASP.NET Core Identity, or any self-hosted IdP where you have your own Users database tables, you'll need to query your own database to get user details (username, display-name, etc) (though normally you would store current-user-details in a structured security-token (JWT, SAML, ClaimsIdentity/ClaimsPrincipal, AuthenticationTicket, etc) when the user authenticates a new session so you avoid hitting your database on every request)...

... but when you use a non-local OIDC IdP (like Okta) those user details are stored and managed by Okta and also held by the client and then forwarded to you via the front-channel, or passed directly via the back-channel; those user details ("claims" in OIDC, known as "assertions" in SAML) are contained in 2 separate tokens: First, the id_token holds non-security-related user profile fields, like display-name and avatar image URI, while the access_token holds security claims and scopes (e.g. user role membership, account-is-locked-out status, etc). If your clients don't send you an id_token then you can always use the IdP's user-info endpoint to get the same data yourself (don't forget to cache it locally, otherwise it's even more expensive than hitting a local DB on every request).

SAML works very similar to OIDC, just with its own terminology for almost identical concepts. As an aside, I'm far more familiar with OIDC and OAuth2 than SAML (with which I have zero experience with), but AFAIK SAML uses a single token for both identity-claims and security-claims assertions whereas OIDC splits it up into id_token and access_token (I might be wrong on this...)

How should we approach authorization in this scenario?

Use declarative Authorization Policies which validate/authenticate based on specific user security claims in the access_token (do not use the id_token for authorization).

But while you can use the same C# Attributes for declarative authorization in your server-side code, be mindful that ASP.NET Web API clients and ASP.NET MVC browser visitors do things differently:

  • In ASP.NET Web API, clients might be an user-interactive smartphone app or desktop command-line program (think like Azure PowerShell) - or it could be a headless background-worker or daemon process. Most importantly: all these clients share the ability to store secrets like Bearer Tokens locally and securely as they're programs running with some kind of read/write access to their local-disk.

    • (While JS SPA clients use ASP.NET Web API, they don't have the ability to store secrets, so they're a special case that I discuss later on).
    • These clients all send their access_token in the HTTP Authorization: Bearer 3q2+7w== request header, and ASP.NET Core's built-in JWT authX features will compare the claims in the access_token with your declared policies and use that to accept or reject the HTTP request.
  • Whereas ASP.NET MVC clients are all web-browsers (Chrome, Firefox, etc) which use traditional HTTP cookie-based authX + sessions.

    • Browser clients have their access_token (and optionally their id_token) stored verbatim inside their ASP.NET security cookie - or the ASP.NET security cookie stores some short reference to a persisted token cached server-side to prevent cookie bloat.
      • This security cookie is symmetrically encrypted by your web-application and marked HTTP-only, so even hostile injected scripts (XSS, etc) in a browser-page cannot access and read those token strings.
      • Caution: because access_token and id_token blobs can be quite large (easily multiple kilobytes) you'll hit browsers' cookie length limits (4096 bytes?), so many web-applications just store all tokens privately (in Redis or memcached or something) and merely store a reference to the cached tokens.
  • When it comes to JS SPAs (Angular, etc) things are complicated: the client is ostensibly a user's web-browser (well, it's actually JavaScript code in the browser) and so it will make fetch-based requests in the background (instead of browser foreground "top level" Document requests) using an access_token in the Authorization header instead of using cookies (which wouldn't be accessible to scripts anyway, due to it being HTTP-only and encrypted, assuming the even were in the ASP.NET security cookie in the first place) - but because JavaScript clients simply do not have any way of securely storing secrets locally (window.localStorage is not private), so SPAs had their own separate OIDC flow: the Implicit Flow, but it's basically insecure - that, combined with browsers' disabling cross-origin cookies and other techniques that OIDC's Front Channel relies on basically means that SPAs now should not make their own fetch HTTP requests to external RPs (i.e. the backend ASP.NET Web API service for an SPA front-end) but should instead be served from a simple ASP.NET MVC which proxies requests to external RPs, and so the SPA actually uses Cookies, not Bearer Tokens - and means that the somewhat nice idea of hosting an SPA in static AWS S3 or Azure Blob storage is now dead.

So if you're planning on building an SPA, you really should give that LeastPrivilege.com article a very good read so you understand how the browser landscape is changing (Dominick Baier is one of IdentityServer's creators and maintainers).


I will say that properly grokking OIDC is hard - I built a self-hosted IdP using IdentityServer4 and while I got something-out-the-door after a few months it still easily took me over a year to truly understand what was actually going on.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM