Merge pull request #1217 from spotify/rugvip/authdocs

docs/auth: add overview and implementation docs
This commit is contained in:
Patrik Oldsberg
2020-06-09 22:20:18 +02:00
committed by Nikita Nek Dudnik
parent c3f0759c2d
commit 1550535d45
26 changed files with 1074 additions and 364 deletions
+51 -20
View File
@@ -2,17 +2,21 @@
## Passport
We chose [Passport](http://www.passportjs.org/) as our authentication platform due to its comprehensive set of supported authentication [strategies](http://www.passportjs.org/packages/).
We chose [Passport](http://www.passportjs.org/) as our authentication platform
due to its comprehensive set of supported authentication
[strategies](http://www.passportjs.org/packages/).
## How to add a new strategy provider
### Quick guide
[1.](#installing-the-dependencies) Install the passport-js based provider package.
[1.](#installing-the-dependencies) Install the passport-js based provider
package.
[2.](#create-implementation) Create a new folder structure for the provider.
[3.](#adding-an-oauth-based-provider) Implement the provider, extending the suitable framework if needed.
[3.](#adding-an-oauth-based-provider) Implement the provider, extending the
suitable framework if needed.
[4.](#hook-it-up-to-the-backend) Add the provider to the backend.
@@ -26,7 +30,8 @@ yarn add @types/passport-provider-a
### Create implementation
Make a new folder with the name of the provider following the below file structure:
Make a new folder with the name of the provider following the below file
structure:
```bash
plugins/auth-backend/src/providers/providerA
@@ -34,13 +39,16 @@ plugins/auth-backend/src/providers/providerA
└── provider.ts
```
**`plugins/auth-backend/src/providers/providerA/provider.ts`** defines the provider class which implements a handler for the chosen framework.
**`plugins/auth-backend/src/providers/providerA/provider.ts`** defines the
provider class which implements a handler for the chosen framework.
#### Adding an OAuth based provider
If we're adding an `OAuth` based provider we would implement the [OAuthProviderHandlers](#OAuthProviderHandlers) interface.
If we're adding an `OAuth` based provider we would implement the
[OAuthProviderHandlers](#OAuthProviderHandlers) interface.
The provider class takes the provider's configuration as a class parameter. It also imports the `Strategy` from the passport package.
The provider class takes the provider's configuration as a class parameter. It
also imports the `Strategy` from the passport package.
```ts
import { Strategy as ProviderAStrategy } from 'passport-provider-a';
@@ -64,9 +72,11 @@ export class ProviderAAuthProvider implements OAuthProviderHandlers {
#### Adding an non-OAuth based provider
_**Note**: We have prioritized OAuth-based providers and non-OAuth providers should be considered experimental._
_**Note**: We have prioritized OAuth-based providers and non-OAuth providers
should be considered experimental._
An non-`OAuth` based provider could implement [AuthProviderRouteHandlers](#AuthProviderRouteHandlers) instead.
An non-`OAuth` based provider could implement
[AuthProviderRouteHandlers](#AuthProviderRouteHandlers) instead.
```ts
export class ProviderAAuthProvider implements AuthProviderRouteHandlers {
@@ -90,9 +100,12 @@ export class ProviderAAuthProvider implements AuthProviderRouteHandlers {
#### Create method
Each provider exports a create method that creates the provider instance, optionally extending a supported authorization framework. This method exists to allow for flexibility if additional frameworks are supported in the future.
Each provider exports a create method that creates the provider instance,
optionally extending a supported authorization framework. This method exists to
allow for flexibility if additional frameworks are supported in the future.
Implementing OAuth by returning an instance of `OAuthProvider` based of the provider's class:
Implementing OAuth by returning an instance of `OAuthProvider` based of the
provider's class:
```ts
export function createProviderAProvider(config: AuthProviderConfig) {
@@ -102,7 +115,9 @@ export function createProviderAProvider(config: AuthProviderConfig) {
}
```
Not extending with OAuth, the main difference here is that the create method is returning a instance of the class without adding the OAuth authorization framework to it.
Not extending with OAuth, the main difference here is that the create method is
returning a instance of the class without adding the OAuth authorization
framework to it.
```ts
export function createProviderAProvider(config: AuthProviderConfig) {
@@ -112,14 +127,22 @@ export function createProviderAProvider(config: AuthProviderConfig) {
#### Verify Callback
> Strategies require what is known as a verify callback. The purpose of a verify callback is to find the user that possesses a set of credentials.
> When Passport authenticates a request, it parses the credentials contained in the request. It then invokes the verify callback with those credentials as arguments [...]. If the credentials are valid, the verify callback invokes done to supply Passport with the user that authenticated.
> Strategies require what is known as a verify callback. The purpose of a verify
> callback is to find the user that possesses a set of credentials. When
> Passport authenticates a request, it parses the credentials contained in the
> request. It then invokes the verify callback with those credentials as
> arguments [...]. If the credentials are valid, the verify callback invokes
> done to supply Passport with the user that authenticated.
>
> If the credentials are not valid (for example, if the password is incorrect), done should be invoked with false instead of a user to indicate an authentication failure.
> If the credentials are not valid (for example, if the password is incorrect),
> done should be invoked with false instead of a user to indicate an
> authentication failure.
>
> http://www.passportjs.org/docs/configure/
**`plugins/auth-backend/src/providers/providerA/index.ts`** is simply re-exporting the create method to be used for hooking the provider up to the backend.
**`plugins/auth-backend/src/providers/providerA/index.ts`** is simply
re-exporting the create method to be used for hooking the provider up to the
backend.
```ts
export { createProviderAProvider } from './provider';
@@ -127,7 +150,9 @@ export { createProviderAProvider } from './provider';
### Hook it up to the backend
**`plugins/auth-backend/src/providers/config.ts`** The provider needs to be configured properly so you need to add it to the list of configured providers, all of which implement [AuthProviderConfig](#AuthProviderConfig):
**`plugins/auth-backend/src/providers/config.ts`** The provider needs to be
configured properly so you need to add it to the list of configured providers,
all of which implement [AuthProviderConfig](#AuthProviderConfig):
```ts
export const providers = [
@@ -138,7 +163,10 @@ export const providers = [
},
```
**`plugins/auth-backend/src/providers/factories.ts`** When the `auth-backend` starts it sets up routing for all the available providers by calling `createAuthProviderRouter` on each provider. You need to import the create method from the provider and add it to the factory:
**`plugins/auth-backend/src/providers/factories.ts`** When the `auth-backend`
starts it sets up routing for all the available providers by calling
`createAuthProviderRouter` on each provider. You need to import the create
method from the provider and add it to the factory:
```ts
import { createProviderAProvider } from './providerA';
@@ -157,11 +185,14 @@ router.post('/auth/providerA/logout');
router.get('/auth/providerA/refresh'); // if supported
```
As you can see each endpoint is prefixed with both `/auth` and its provider name.
As you can see each endpoint is prefixed with both `/auth` and its provider
name.
### Test the new provider
You can `curl -i localhost:7000/auth/providerA/start` and which should provide a `302` redirect with a `Location` header. Paste the url from that header into a web browser and you should be able to trigger the authorization flow.
You can `curl -i localhost:7000/auth/providerA/start` and which should provide a
`302` redirect with a `Location` header. Paste the url from that header into a
web browser and you should be able to trigger the authorization flow.
---
+26
View File
@@ -0,0 +1,26 @@
# Glossary
- **Popup** - A separate browser window opened on top of the previous one.
- **OAuth** - More specifically OAuth 2.0, a standard protocol for
authorization. See [oauth.net/2/](https://oauth.net/2/).
- **OpenID Connect** - A layer on top of OAuth which standardises
authentication. See
[en.wikipedia.org/wiki/OpenID_Connect](https://en.wikipedia.org/wiki/OpenID_Connect).
- **JWT** - JSON Web Token, a popular JSON based token format that is commonly
encrypted and/or signed, see
[en.wikipedia.org/wiki/JSON_Web_Token](https://en.wikipedia.org/wiki/JSON_Web_Token)
- **Scope** - A string that describes a certain type of access that can be
granted to a user using OAuth.
- **Access token** - A token that gives access to perform actions on behalf of a
user. It will commonly have a short expiry time, and be limited to a set of
scopes. Part of the OAuth protocol.
- **ID token** - A JWT used to prove a user's identity, containing for example
the user's email. Part of OpenID Connect.
- **Offline access** - OAuth flow that results in both a refresh and access
token, where the refresh token has a long expiration or never expires, and can
be used to request more access tokens in the future. This lets the user go
"offline" with respect to the token issuer, but still be able to request more
tokens at a later time without further direct interaction for the user.
- **Code grant** - OAuth flow where the client receives an authorization code
that is passed to the backend to be exchanged for an access token and possibly
refresh token.
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

+151
View File
@@ -0,0 +1,151 @@
# OAuth and OpenID Connect
This section describes how Backstage allows plugins to request OAuth Access
Tokens and OpenID Connect ID Tokens on behalf of the user, to be used for auth
to various third party APIs.
## Summary
There are occasions when the user wants to perform actions towards third party
services that require authorization via OAuth. Backstage provides standardized
[Utility APIs](../getting-started/utility-apis.md) such as the
[GoogleAuthApi](../../packages/core-api/src/apis/definitions/auth.ts) for that
use-case. Backstage also includes a set of implementations of these APIs that
integrate with the [auth-backend](../../plugins/auth-backend) plugin to provide
a popup-based OAuth flow.
## Background
Access control in OAuth is implemented in terms of scope, which is a list of
permissions given to the app. An OAuth service can issue Access Tokens that are
tied to a certain set of scopes, such as viewing profile information, reading
and/or writing user data in the service. The scope format and handling is
specific to each OAuth provider, and the set of available scopes are typically
found in the documentation describing the auth solution of the provider, for
example
[developers.google.com/identity/protocols/oauth2/scopes](https://developers.google.com/identity/protocols/oauth2/scopes).
As a part of logging in with an OAuth provider, the user needs to consent to
both the login itself and the set of scopes that the app is requesting to use.
This is done by loading a page provided by the OAuth provider, where a user can
choose an account to log in with, and accept or reject the request. If the user
accepts the login request, a token is issued, and any holder of the token can
use it to make authenticated requests towards the third party service.
## OAuth in @backstage/core-api and auth-backend
The default OAuth implementation in Backstage is based on an OAuth server-side
offline access flow, which means that it uses the backend as a helper in order
to trade credentials. A benefit of this type of flow is that it does not require
the use of third party cookies, and is robust on a wide selection of browsers
and privacy browsing plugins, strict security settings, etc.
The implementation also uses a popup-based flow, where auth requests are handled
in a new popup window that is opened by the app. By using a popup-based flow it
is possible to request authentication at any point in the app, without requiring
a redirect. Because of this there is no need to ask for all scopes upfront, or
interrupt the app with a redirect and forcing plugin authors to take care in
restoring state after a redirect has been make. All in all it makes it much
easier to make authenticated requests inside a plugin.
## OAuth Flow
The following describes the OAuth flow implemented by the
[auth-backend](../../plugins/auth-backend) and
[DefaultAuthConnector](../../packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts)
in `@backstage/core-api`.
Component and APIs can request Access or ID Tokens from any available Auth
provider. If there already exists a cached fresh token that covers (at least)
the requested scopes, it will be returned immediately. If the OAuth provider
implements token refreshes, this check will also trigger a token refresh attempt
if no session is a available.
If new scopes are requested, or the user is not yet logged in with that
provider, a dialog is shown informing the user that they need to log in with the
specified provider. If the user agrees to continue, a separate popup window is
opened that implements the entire consent flow.
The popup window is pointed to the `/start` endpoint of the auth provider in the
`auth-backend` plugin, which then redirects to the OAuth consent screen of the
provider. The consent screen is controlled by the OAuth provider, and will do
things like prompting the user to log in with an account, and possibly reviewing
the set of requested scopes. If the login request is accepted, the popup window
will be redirected back to the `/handler/frame` endpoint of the auth backend.
The redirect URL will contain a short-term authorization code, which is picked
up by the backend and exchanged for long-term tokens via a call to the OAuth
provider. The Access and possibly ID Token is then handed back to the main
Backstage page via `postMessage`. If the OAuth provider implements offline
refresh, a refresh token will be stored in an HTTP-only cookie scoped to the
specific provider in the `auth-backend` plugin.
To protect against certain attacks, the above flow also includes a simple nonce
check and a lightweight CSRF protection header. The nonce check is done to
protect against attacks where an attacker tricks a user to log in with an
account of the attacker's choosing in order to gather data. In the first part of
the flow where the popup is directed to the `/start` endpoint, a nonce is
generated and placed in both a cookie and the OAuth state. The nonces received
in the cookie and OAuth state in the redirect handler are then checked, and the
auth attempt will fail if they're not valid. The CSRF protection for the
`/refresh` and `/logout` endpoints is implemented by simply checking for the
presence of a `X-Requested-With` header.
The target origin of the `postMessage` is also of importance to keep the flow
secure. It is configured to a single value for each auth provider and
environment. Without a single configured origin, any page could open a popup and
request an access token.
### Sequence Diagram
The following diagram visualizes the flow described in the previous section.
<!--
@startuml oauth-popup-flow
skinparam monochrome true
skinparam shadowing false
skinparam backgroundColor #fefefe
skinparam defaultFontName Segoe UI, Helvetica, Arial, sans-serif
title OAuth Consent and Refresh Flow
participant Browser
participant "Popup Window" as Popup
participant "auth-backend plugin" as Backend
control "Consent Screen" as Consent
entity "OAuth Provider" as Provider
note over Browser: Components on page ask for an\naccess token with greater\nscope than the existing session.
Browser -> Popup: Open popup
Popup -> Backend: GET /auth/<provider>/start?scope=some%20scopes
Popup <- Backend: Redirect to consent screen with\nrandom nonce in OAuth state and\nshort-lived cookie with the same nonce.
Popup -> Consent: GET /consent_url?redirect_uri=<redirect_uri>?nonce=<n>\nwhere redirect_uri=<app-origin>/auth/<provider>/handler/frame
note over Consent: User consents to\naccess the new scope.
Popup <- Consent: Redirect to given redirect URL, with authorization code
Popup -> Backend: GET /auth/<provider>/handler/frame?code=<c>&nonce=<n>\nRequest includes the previously set none cookie
note over Backend: Verify that the nonce in the cookie\nmatches the nonce in the OAuth state
Backend -> Provider: Authorization Code\nClient ID\nClient Secret
note over Provider: Verify and generate tokens
Backend <- Provider: Access Token\n(ID Token)\n(Refresh Token)\nScope\nExpire Time
Popup <- Backend: Small HTML page with inlined response payload\nStore Refresh Token in HTTP-only cookie
Browser <- Popup: postMessage() with tokens and info or error
Popup -> Popup: Close self
note over Browser: A later point when a refresh\n is needed. Either because of\n a reload or an expiring session.
Browser -> Backend: GET /auth/<provider>/token\nRefresh Token cookie included
Backend -> Provider: Refresh Token\nClient ID\nClient Secret
Backend <- Provider: Access Token\n(ID Token)\nScope\nExpire Time
Browser <- Backend: Tokens and info
@enduml
-->
![](oauth-popup-flow.svg)
+44
View File
@@ -0,0 +1,44 @@
# User Authentication and Authorization in Backstage
## Summary
The purpose of the Auth APIs in Backstage are to identify the user, and to
provide a way for plugins to request access to 3rd party services on behalf of
the user (OAuth). This documentation focuses on the implementation of that
solution and how to extend it. For documentation on how to consume the Auth APIs
in a plugin, see [TODO](#TODO).
### Accessing Third Party Services
The main pattern for talking to third party services in Backstage is
user-to-server requests, where short-lived OAuth Access Tokens are requested by
plugins to authenticate calls to external services. These calls can be made
either directly to the services or through a backend plugin or service.
By relying on user-to-server calls we keep the coupling between the frontend and
backend low, and provide a much lower barrier for plugins to make use of third
party services. This is in comparison to for example a session-based system,
where access tokens are stored server-side. Such a solution would require a much
deeper coupling between the auth backend plugin, its session storage, and other
backend plugins or separate services. A goal of Backstage is to make it as easy
as possible to create new plugins, and an auth solution based on user-to-server
OAuth helps in that regard.
The method with which frontend plugins request access to third party services is
through [Utility APIs](../getting-started/utility-apis.md) for each service
provider. For a full list of providers, see [TODO](#TODO).
### Identity - TODO
This documentation currently only covers the OAuth use-case, as identity
management is not settled yet and part of an
[upcoming milestone](https://github.com/spotify/backstage/milestone/12).
## Further Reading
More details are provided in dedicated sections of the documentation.
- [OAuth](./oauth): Description of the generic OAuth flow implemented by the
[auth-backend](../../plugins/auth-backend).
- [Glossary](./glossary): Glossary of some common terms related to the auth
flows.