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/cloudhub/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 │ │ CloudHub │ │Provider│ └─────────┘ └───────────┘ └────────┘ │ │ │ ├─────── GET /auth ─────────▶ │ │ │ │ │ │ │ ◀ ─ ─ ─302 to Provider ─ ─ ┤ │ │ │ │ │ │ │ ├──────────────── GET /auth w/ callback ─────────────────────▶ │ │ │ │ │ │ ◀─ ─ ─ ─ ─ ─ ─ 302 to CloudHub 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 CloudHub, 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 CloudHub 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. CloudHub 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 CloudHub 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 `/cloudhub/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
- Variables
- type Auth0
- type AuthMux
- type Authenticator
- type Claims
- type ExtendedProvider
- type Generic
- func (g *Generic) Config() *oauth2.Config
- func (g *Generic) Group(provider *http.Client) (string, error)
- func (g *Generic) GroupFromClaims(claims gojwt.MapClaims) (string, error)
- func (g *Generic) ID() string
- func (g *Generic) Name() string
- func (g *Generic) PrincipalID(provider *http.Client) (string, error)
- func (g *Generic) PrincipalIDFromClaims(claims gojwt.MapClaims) (string, error)
- func (g *Generic) Scopes() []string
- func (g *Generic) Secret() string
- type Github
- type Google
- type Heroku
- type JWK
- type JWKS
- type JWT
- func (j *JWT) Create(ctx context.Context, user Principal) (Token, error)
- func (j *JWT) ExtendedPrincipal(ctx context.Context, principal Principal, extension time.Duration) (Principal, error)
- func (j *JWT) GetClaims(tokenString string) (gojwt.MapClaims, error)
- func (j *JWT) KeyFunc(token *gojwt.Token) (interface{}, error)
- func (j *JWT) KeyFuncRS256(token *gojwt.Token) (interface{}, error)
- func (j *JWT) ValidClaims(jwtToken Token, lifespan time.Duration, alg gojwt.Keyfunc) (Principal, error)
- func (j *JWT) ValidPrincipal(ctx context.Context, jwtToken Token, lifespan time.Duration) (Principal, error)
- type Mux
- type Principal
- type Provider
- type Token
- type Tokenizer
- type UserEmail
Constants ¶
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 )
const ( // HerokuAccountRoute is required for interacting with Heroku API HerokuAccountRoute string = "https://api.heroku.com/account" )
const TenMinutes = 10 * time.Minute
TenMinutes is the default length of time to get a response back from the OAuth provider
Variables ¶
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") )
var DefaultNowTime = func() time.Time { return time.Now().UTC() }
DefaultNowTime returns UTC time at the present moment
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 ...
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 cloudhub.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 LoginHint string // LoginHint will be included as a parameter during authentication if non-nil // contains filtered or unexported fields }
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 CloudHub 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 cloudhub.Logger, UseIDToken bool, LoginHint string, client *http.Client) *AuthMux
NewAuthMux constructs a Mux handler that checks a cookie against the authenticator
func (*AuthMux) Callback ¶
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).
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.
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 cloudhub.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) GroupFromClaims ¶
GroupFromClaims verifies an optional id_token, extracts the email address of the user and splits off the domain part
func (*Generic) PrincipalID ¶
PrincipalID returns the email address of the user.
func (*Generic) PrincipalIDFromClaims ¶
PrincipalIDFromClaims verifies an optional id_token and extracts email address of the user
type Github ¶
type Github struct { ClientID string ClientSecret string Orgs []string // Optional github organization checking Logger cloudhub.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) Group ¶
Group returns a comma delimited string of Github organizations that a user belongs to in Github
func (*Github) PrincipalID ¶
PrincipalID returns the github email address of the user.
type Google ¶
type Google struct { ClientID string ClientSecret string RedirectURL string Domains []string // Optional google email domain checking Logger cloudhub.Logger }
Google is an oauth2 provider supporting google.
func (*Google) PrincipalID ¶
PrincipalID returns the google email address of the user.
func (*Google) Scopes ¶
Scopes for google is only the email address Documentation is here: https://developers.google.com/+/web/api/rest/oauth#email
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 cloudhub.Logger }
Heroku is an OAuth2 Provider allowing users to authenticate with Heroku to gain access to CloudHub
func (*Heroku) PrincipalID ¶
PrincipalID returns the Heroku email address of the user.
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 JWT ¶
JWT represents a javascript web token that can be validated or marshaled into string.
func NewJWT ¶
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 ¶
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) KeyFuncRS256 ¶
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 ¶
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.