oauth2

package
v0.0.0-...-0184392 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 17, 2020 License: AGPL-3.0, AGPL-3.0-or-later Imports: 21 Imported by: 0

Documentation

Overview

Package oauth2 provides http.Handlers necessary for implementing Oauth2 authentication with multiple Providers.

This is how the pieces of this package fit together:

┌────────────────────────────────────────┐
│github.com/snetsystems/cmp/oauth2        │
├────────────────────────────────────────┴────────────────────────────────────┐
│┌────────────────────┐                                                       │
││   <<interface>>    │        ┌─────────────────────────┐                    │
││   Authenticator    │        │         AuthMux         │                    │
│├────────────────────┤        ├─────────────────────────┤                    │
││Authorize()         │   Auth │+SuccessURL : string     │                    │
││Validate()          ◀────────│+FailureURL : string     │──────────┐         │
||Expire()            |        |+Now : func() time.Time  |          |         |
│└──────────△─────────┘        └─────────────────────────┘          |         |
│           │                               │                       │         |
│           │                               │                       │         │
│           │                               │                       │         │
│           │                       Provider│                       │         │
│           │                           ┌───┘                       │         │
│┌──────────┴────────────┐              │                           ▽         │
││         Tokenizer     │              │                   ┌───────────────┐ │
│├───────────────────────┤              ▼                   │ <<interface>> │ │
││Create()               │      ┌───────────────┐           │   OAuth2Mux   │ │
││ValidPrincipal()       │      │ <<interface>> │           ├───────────────┤ │
│└───────────────────────┘      │   Provider    │           │Login()        │ │
│                               ├───────────────┤           │Logout()       │ │
│                               │ID()           │           │Callback()     │ │
│                               │Scopes()       │           └───────────────┘ │
│                               │Secret()       │                             │
│                               │Authenticator()│                             │
│                               └───────────────┘                             │
│                                       △                                     │
│                                       │                                     │
│             ┌─────────────────────────┼─────────────────────────┐           │
│             │                         │                         │           │
│             │                         │                         │           │
│             │                         │                         │           │
│ ┌───────────────────────┐ ┌──────────────────────┐  ┌──────────────────────┐│
│ │        Github         │ │        Google        │  │        Heroku        ││
│ ├───────────────────────┤ ├──────────────────────┤  ├──────────────────────┤│
│ │+ClientID : string     │ │+ClientID : string    │  │+ClientID : string    ││
│ │+ClientSecret : string │ │+ClientSecret : string│  │+ClientSecret : string││
│ │+Orgs : []string       │ │+Domains : []string   │  └──────────────────────┘│
│ └───────────────────────┘ │+RedirectURL : string │                          │
│                           └──────────────────────┘                          │
└─────────────────────────────────────────────────────────────────────────────┘

The design focuses on an Authenticator, a Provider, and an OAuth2Mux. Their responsibilities, respectively, are to decode and encode secrets received from a Provider, to perform Provider specific operations in order to extract information about a user, and to produce the handlers which persist secrets. To add a new provider, You need only implement the Provider interface, and add its endpoints to the server Mux.

The Oauth2 flow between a browser, backend, and a Provider that this package implements is pictured below for reference.

┌─────────┐                ┌───────────┐                     ┌────────┐
│ Browser │                │    CMP    │                     │Provider│
└─────────┘                └───────────┘                     └────────┘
     │                           │                                │
     ├─────── GET /auth ─────────▶                                │
     │                           │                                │
     │                           │                                │
     ◀ ─ ─ ─302 to Provider  ─ ─ ┤                                │
     │                           │                                │
     │                           │                                │
     ├──────────────── GET /auth w/ callback ─────────────────────▶
     │                           │                                │
     │                           │                                │
     ◀─ ─ ─ ─ ─ ─ ─   302 to CMP Callback  ─ ─ ─ ─ ─ ─ ─ ─ ┤
     │                           │                                │
     │   Code and State from     │                                │
     │        Provider           │                                │
     ├───────────────────────────▶    Request token w/ code &     │
     │                           │             state              │
     │                           ├────────────────────────────────▶
     │                           │                                │
     │                           │          Response with         │
     │                           │              Token             │
     │   Set cookie, Redirect    │◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤
     │           to /            │                                │
     ◀───────────────────────────┤                                │
     │                           │                                │
     │                           │                                │
     │                           │                                │
     │                           │                                │

The browser ultimately receives a cookie from CMP, authorizing it. Its contents are encoded as a JWT whose "sub" claim is the user's email address for whatever provider they have authenticated with. Each request to CMP will validate the contents of this JWT against the `TOKEN_SECRET` and checked for expiration. The JWT's "sub" becomes the https://en.wikipedia.org/wiki/Principal_(computer_security) used for authorization to resources.

The Mux is responsible for providing three http.Handlers for servicing the above interaction. These are mounted at specific endpoints by convention shared with the front end. Any future Provider routes should follow the same convention to ensure compatibility with the front end logic. These routes and their responsibilities are:

/oauth/{provider}/login

The `/oauth` endpoint redirects to the Provider for OAuth. CMP sets the OAuth `state` request parameter to a JWT with a random "sub". Using $TOKEN_SECRET `/oauth/github/callback` can validate the `state` parameter without needing `state` to be saved.

/oauth/{provider}/callback

The `/oauth/github/callback` receives the OAuth `authorization code` and `state`.

First, it will validate the `state` JWT from the `/oauth` endpoint. `JWT` validation only requires access to the signature token. Therefore, there is no need for `state` to be saved. Additionally, multiple CMP servers will not need to share third party storage to synchronize `state`. If this validation fails, the request will be redirected to `/login`.

Secondly, the endpoint will use the `authorization code` to retrieve a valid OAuth token with the `user:email` scope. If unable to get a token from Github, the request will be redirected to `/login`.

Finally, the endpoint will attempt to get the primary email address of the user. Again, if not successful, the request will redirect to `/login`.

The email address is used as the subject claim for a new JWT. This JWT becomes the value of the cookie sent back to the browser. The cookie is valid for thirty days.

Next, the request is redirected to `/`.

For all API calls to `/cmp/v1`, the server checks for the existence and validity of the JWT within the cookie value. If the request did not have a valid JWT, the API returns `HTTP/1.1 401 Unauthorized`.

/oauth/{provider}/logout

Simply expires the session cookie and redirects to `/`.

Index

Constants

View Source
const (
	// DefaultCookieName is the name of the stored cookie
	DefaultCookieName = "session"
	// DefaultInactivityDuration is the duration a token is valid without any new activity
	DefaultInactivityDuration = 5 * time.Minute
)
View Source
const (
	// HerokuAccountRoute is required for interacting with Heroku API
	HerokuAccountRoute string = "https://api.heroku.com/account"
)
View Source
const TenMinutes = 10 * time.Minute

TenMinutes is the default length of time to get a response back from the OAuth provider

Variables

View Source
var (
	// PrincipalKey is used to pass principal
	// via context.Context to request-scoped
	// functions.
	PrincipalKey = principalKey("principal")
	// ErrAuthentication means that oauth2 exchange failed
	ErrAuthentication = errors.New("user not authenticated")
	// ErrOrgMembership means that the user is not in the OAuth2 filtered group
	ErrOrgMembership = errors.New("Not a member of the required organization")
)
View Source
var DefaultNowTime = func() time.Time { return time.Now().UTC() }

DefaultNowTime returns UTC time at the present moment

View Source
var GoogleEndpoint = oauth2.Endpoint{
	AuthURL:  "https://accounts.google.com/o/oauth2/auth",
	TokenURL: "https://accounts.google.com/o/oauth2/token",
}

GoogleEndpoint is Google's OAuth 2.0 endpoint. Copied here to remove tons of package dependencies

Functions

This section is empty.

Types

type Auth0

type Auth0 struct {
	Generic
	Organizations map[string]bool // the set of allowed organizations users may belong to
}

Auth0 ...

func NewAuth0

func NewAuth0(auth0Domain, clientID, clientSecret, redirectURL string, organizations []string, logger cmp.Logger) (Auth0, error)

NewAuth0 ...

func (*Auth0) Group

func (a *Auth0) Group(provider *http.Client) (string, error)

Group ...

func (*Auth0) PrincipalID

func (a *Auth0) PrincipalID(provider *http.Client) (string, error)

PrincipalID ...

type AuthMux

type AuthMux struct {
	Provider   Provider         // Provider is the OAuth2 service
	Auth       Authenticator    // Auth is used to Authorize after successful OAuth2 callback and Expire on Logout
	Tokens     Tokenizer        // Tokens is used to create and validate OAuth2 "state"
	Logger     cmp.Logger       // Logger is used to give some more information about the OAuth2 process
	SuccessURL string           // SuccessURL is redirect location after successful authorization
	FailureURL string           // FailureURL is redirect location after authorization failure
	Now        func() time.Time // Now returns the current time (for testing)
	UseIDToken bool             // UseIDToken enables OpenID id_token support
}

AuthMux services an Oauth2 interaction with a provider and browser and stores the resultant token in the user's browser as a cookie. The benefit of this is that the cookie's authenticity can be verified independently by any CMP instance as long as the Authenticator has no external dependencies (e.g. on a Database).

func NewAuthMux

func NewAuthMux(p Provider, a Authenticator, t Tokenizer, basepath string, l cmp.Logger, UseIDToken bool) *AuthMux

NewAuthMux constructs a Mux handler that checks a cookie against the authenticator

func (*AuthMux) Callback

func (j *AuthMux) Callback() http.Handler

Callback is used by OAuth2 provider after authorization is granted. If granted, Callback will set a cookie with a month-long expiration. It is recommended that the value of the cookie be encoded as a JWT because the JWT can be validated without the need for saving state. The JWT contains the principal's identifier (e.g. email address).

func (*AuthMux) Login

func (j *AuthMux) Login() http.Handler

Login uses a Cookie with a random string as the state validation method. JWTs are a good choice here for encoding because they can be validated without storing state. Login returns a handler that redirects to the providers OAuth login.

func (*AuthMux) Logout

func (j *AuthMux) Logout() http.Handler

Logout handler will expire our authentication cookie and redirect to the successURL

type Authenticator

type Authenticator interface {
	// Validate returns Principal associated with authenticated and authorized
	// entity if successful.
	Validate(context.Context, *http.Request) (Principal, error)
	// Authorize will grant privileges to a Principal
	Authorize(context.Context, http.ResponseWriter, Principal) error
	// Extend will extend the lifetime of a already validated Principal
	Extend(context.Context, http.ResponseWriter, Principal) (Principal, error)
	// Expire revokes privileges from a Principal
	Expire(http.ResponseWriter)
}

Authenticator represents a service for authenticating users.

func NewCookieJWT

func NewCookieJWT(secret string, lifespan time.Duration) Authenticator

NewCookieJWT creates an Authenticator that uses cookies for auth

type Claims

type Claims struct {
	gojwt.StandardClaims
	// We were unable to find a standard claim at https://www.iana.org/assignments/jwt/jwt.xhtml
	// that felt appropriate for Organization. As a result, we added a custom `org` field.
	Organization string `json:"org,omitempty"`
	// We were unable to find a standard claim at https://www.iana.org/assignments/jwt/jwt.xhtml
	// that felt appropriate for a users Group(s). As a result we added a custom `grp` field.
	// Multiple groups may be specified by comma delimiting the various group.
	//
	// The singlular `grp` was chosen over the `grps` to keep consistent with the JWT naming
	// convention (it is common for singlularly named values to actually be arrays, see `given_name`,
	// `family_name`, and `middle_name` in the iana link provided above). I should add the discalimer
	// I'm currently sick, so this thought process might be off.
	Group string `json:"grp,omitempty"`
}

Claims extends jwt.StandardClaims' Valid to make sure claims has a subject.

func (*Claims) Valid

func (c *Claims) Valid() error

Valid adds an empty subject test to the StandardClaims checks.

type ExtendedProvider

type ExtendedProvider interface {
	Provider
	// get PrincipalID from id_token
	PrincipalIDFromClaims(claims gojwt.MapClaims) (string, error)
	GroupFromClaims(claims gojwt.MapClaims) (string, error)
}

ExtendedProvider extendts the base Provider interface with optional methods

type Generic

type Generic struct {
	PageName       string // Name displayed on the login page
	ClientID       string
	ClientSecret   string
	RequiredScopes []string
	Domains        []string // Optional email domain checking
	RedirectURL    string
	AuthURL        string
	TokenURL       string
	APIURL         string // APIURL returns OpenID Userinfo
	APIKey         string // APIKey is the JSON key to lookup email address in APIURL response
	Logger         cmp.Logger
}

Generic provides OAuth Login and Callback server and is modeled after the Github OAuth2 provider. Callback will set an authentication cookie. This cookie's value is a JWT containing the user's primary email address.

func (*Generic) Config

func (g *Generic) Config() *oauth2.Config

Config is the Generic OAuth2 exchange information and endpoints

func (*Generic) Group

func (g *Generic) Group(provider *http.Client) (string, error)

Group returns the domain that a user belongs to in the the generic OAuth.

func (*Generic) GroupFromClaims

func (g *Generic) GroupFromClaims(claims gojwt.MapClaims) (string, error)

GroupFromClaims verifies an optional id_token, extracts the email address of the user and splits off the domain part

func (*Generic) ID

func (g *Generic) ID() string

ID returns the generic application client id

func (*Generic) Name

func (g *Generic) Name() string

Name is the name of the provider

func (*Generic) PrincipalID

func (g *Generic) PrincipalID(provider *http.Client) (string, error)

PrincipalID returns the email address of the user.

func (*Generic) PrincipalIDFromClaims

func (g *Generic) PrincipalIDFromClaims(claims gojwt.MapClaims) (string, error)

PrincipalIDFromClaims verifies an optional id_token and extracts email address of the user

func (*Generic) Scopes

func (g *Generic) Scopes() []string

Scopes for generic provider required of the client.

func (*Generic) Secret

func (g *Generic) Secret() string

Secret returns the generic application client secret

type Github

type Github struct {
	ClientID     string
	ClientSecret string
	Orgs         []string // Optional github organization checking
	Logger       cmp.Logger
}

Github provides OAuth Login and Callback server. Callback will set an authentication cookie. This cookie's value is a JWT containing the user's primary Github email address.

func (*Github) Config

func (g *Github) Config() *oauth2.Config

Config is the Github OAuth2 exchange information and endpoints.

func (*Github) Group

func (g *Github) Group(provider *http.Client) (string, error)

Group returns a comma delimited string of Github organizations that a user belongs to in Github

func (*Github) ID

func (g *Github) ID() string

ID returns the github application client id.

func (*Github) Name

func (g *Github) Name() string

Name is the name of the provider.

func (*Github) PrincipalID

func (g *Github) PrincipalID(provider *http.Client) (string, error)

PrincipalID returns the github email address of the user.

func (*Github) Scopes

func (g *Github) Scopes() []string

Scopes for github is only the email address and possible organizations if we are filtering by organizations.

func (*Github) Secret

func (g *Github) Secret() string

Secret returns the github application client secret.

type Google

type Google struct {
	ClientID     string
	ClientSecret string
	RedirectURL  string
	Domains      []string // Optional google email domain checking
	Logger       cmp.Logger
}

Google is an oauth2 provider supporting google.

func (*Google) Config

func (g *Google) Config() *oauth2.Config

Config is the Google OAuth2 exchange information and endpoints

func (*Google) Group

func (g *Google) Group(provider *http.Client) (string, error)

Group returns the string of domain a user belongs to in Google

func (*Google) ID

func (g *Google) ID() string

ID returns the google application client id

func (*Google) Name

func (g *Google) Name() string

Name is the name of the provider

func (*Google) PrincipalID

func (g *Google) PrincipalID(provider *http.Client) (string, error)

PrincipalID returns the google email address of the user.

func (*Google) Scopes

func (g *Google) Scopes() []string

Scopes for google is only the email address Documentation is here: https://developers.google.com/+/web/api/rest/oauth#email

func (*Google) Secret

func (g *Google) Secret() string

Secret returns the google application client secret

type Heroku

type Heroku struct {
	// OAuth2 Secrets
	ClientID     string
	ClientSecret string

	Organizations []string // set of organizations permitted to access the protected resource. Empty means "all"

	Logger cmp.Logger
}

Heroku is an OAuth2 Provider allowing users to authenticate with Heroku to gain access to CMP

func (*Heroku) Config

func (h *Heroku) Config() *oauth2.Config

Config returns the OAuth2 exchange information and endpoints

func (*Heroku) Group

func (h *Heroku) Group(provider *http.Client) (string, error)

Group returns the Heroku organization that user belongs to.

func (*Heroku) ID

func (h *Heroku) ID() string

ID returns the Heroku application client ID

func (*Heroku) Name

func (h *Heroku) Name() string

Name returns the name of this provider (heroku)

func (*Heroku) PrincipalID

func (h *Heroku) PrincipalID(provider *http.Client) (string, error)

PrincipalID returns the Heroku email address of the user.

func (*Heroku) Scopes

func (h *Heroku) Scopes() []string

Scopes for heroku is "identity" which grants access to user account information. This will grant us access to the user's email address which is used as the Principal's identifier.

func (*Heroku) Secret

func (h *Heroku) Secret() string

Secret returns the Heroku application client secret

type JWK

type JWK struct {
	Kty string   `json:"kty"`
	Use string   `json:"use"`
	Alg string   `json:"alg"`
	Kid string   `json:"kid"`
	X5t string   `json:"x5t"`
	N   string   `json:"n"`
	E   string   `json:"e"`
	X5c []string `json:"x5c"`
}

JWK defines a JSON Web KEy nested struct

type JWKS

type JWKS struct {
	Keys []JWK `json:"keys"`
}

JWKS defines a JKW[]

type JWT

type JWT struct {
	Secret  string
	Jwksurl string
	Now     func() time.Time
}

JWT represents a javascript web token that can be validated or marshaled into string.

func NewJWT

func NewJWT(secret string, jwksurl string) *JWT

NewJWT creates a new JWT using time.Now secret is used for signing and validating signatures (HS256/HMAC) jwksurl is used for validating RS256 signatures.

func (*JWT) Create

func (j *JWT) Create(ctx context.Context, user Principal) (Token, error)

Create creates a signed JWT token from user that expires at Principal's ExpireAt time.

func (*JWT) ExtendedPrincipal

func (j *JWT) ExtendedPrincipal(ctx context.Context, principal Principal, extension time.Duration) (Principal, error)

ExtendedPrincipal sets the expires at to be the current time plus the extention into the future

func (*JWT) GetClaims

func (j *JWT) GetClaims(tokenString string) (gojwt.MapClaims, error)

GetClaims extracts claims from id_token

func (*JWT) KeyFunc

func (j *JWT) KeyFunc(token *gojwt.Token) (interface{}, error)

KeyFunc verifies HMAC or RSA/RS256 signatures

func (*JWT) KeyFuncRS256

func (j *JWT) KeyFuncRS256(token *gojwt.Token) (interface{}, error)

KeyFuncRS256 verifies RS256 signed JWT tokens, it looks up the signing key in the key discovery service

func (*JWT) ValidClaims

func (j *JWT) ValidClaims(jwtToken Token, lifespan time.Duration, alg gojwt.Keyfunc) (Principal, error)

ValidClaims validates a token with StandardClaims

func (*JWT) ValidPrincipal

func (j *JWT) ValidPrincipal(ctx context.Context, jwtToken Token, lifespan time.Duration) (Principal, error)

ValidPrincipal checks if the jwtToken is signed correctly and validates with Claims. lifespan is the maximum valid lifetime of a token. If the lifespan is 0 then the auth lifespan duration is not checked.

type Mux

type Mux interface {
	Login() http.Handler
	Logout() http.Handler
	Callback() http.Handler
}

Mux is a collection of handlers responsible for servicing an Oauth2 interaction between a browser and a provider

type Principal

type Principal struct {
	Subject      string
	Issuer       string
	Organization string
	Group        string
	ExpiresAt    time.Time
	IssuedAt     time.Time
}

Principal is any entity that can be authenticated

type Provider

type Provider interface {
	// ID is issued to the registered client by the authorization (RFC 6749 Section 2.2)
	ID() string
	// Secret associated is with the ID (Section 2.2)
	Secret() string
	// Scopes is used by the authorization server to "scope" responses (Section 3.3)
	Scopes() []string
	// Config is the OAuth2 configuration settings for this provider
	Config() *oauth2.Config
	// PrincipalID with fetch the identifier to be associated with the principal.
	PrincipalID(provider *http.Client) (string, error)
	// Name is the name of the Provider
	Name() string
	// Group is a comma delimited list of groups and organizations for a provider
	// TODO: This will break if there are any group names that contain commas.
	//       I think this is okay, but I'm not 100% certain.
	Group(provider *http.Client) (string, error)
}

Provider are the common parameters for all providers (RFC 6749)

type Token

type Token string

Token represents a time-dependent reference (i.e. identifier) that maps back to the sensitive data through a tokenization system

type Tokenizer

type Tokenizer interface {
	// Create issues a token at Principal's IssuedAt that lasts until Principal's ExpireAt
	Create(context.Context, Principal) (Token, error)
	// ValidPrincipal checks if the token has a valid Principal and requires
	// a lifespan duration to ensure it complies with possible server runtime arguments.
	ValidPrincipal(ctx context.Context, token Token, lifespan time.Duration) (Principal, error)
	// ExtendedPrincipal adds the extention to the principal's lifespan.
	ExtendedPrincipal(ctx context.Context, principal Principal, extension time.Duration) (Principal, error)
	// GetClaims returns a map with verified claims
	GetClaims(tokenString string) (gojwt.MapClaims, error)
}

Tokenizer substitutes a sensitive data element (Principal) with a non-sensitive equivalent, referred to as a token, that has no extrinsic or exploitable meaning or value.

type UserEmail

type UserEmail struct {
	Email    *string `json:"email,omitempty"`
	Primary  *bool   `json:"primary,omitempty"`
	Verified *bool   `json:"verified,omitempty"`
}

UserEmail represents user's email address

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL