storj/satellite/oidc/endpoint.go
Mya 4a110b266e satellite/console: added oidc endpoints
This change adds endpoints for supporting OpenID Connect (OIDC) and
OAuth requests. This allows application developers to easily
develop apps with Storj using common mechanisms for authentication
and authorization.

Change-Id: I2a76d48bd1241367aa2d1e3309f6f65d6d6ea4dc
2022-03-16 12:01:26 +00:00

199 lines
5.5 KiB
Go

// Copyright (C) 2022 Storj Labs, Inc.
// See LICENSE for copying information.
package oidc
import (
"bytes"
"encoding/json"
"net/http"
"strings"
"time"
"github.com/go-oauth2/oauth2/v4"
"github.com/go-oauth2/oauth2/v4/manage"
"github.com/go-oauth2/oauth2/v4/server"
"github.com/spacemonkeygo/monkit/v3"
"storj.io/common/uuid"
"storj.io/storj/satellite/console"
)
var (
mon = monkit.Package()
)
// NewEndpoint constructs an OpenID identity provider.
func NewEndpoint(externalAddress string, oidcService *Service, service *console.Service, codeExpiry, accessTokenExpiry, refreshTokenExpiry time.Duration) *Endpoint {
manager := manage.NewManager()
tokenStore := oidcService.TokenStore()
manager.MapClientStorage(oidcService.ClientStore())
manager.MapTokenStorage(tokenStore)
manager.MapAuthorizeGenerate(&UUIDAuthorizeGenerate{})
manager.SetAuthorizeCodeExp(codeExpiry)
manager.MapAccessGenerate(&MacaroonAccessGenerate{Service: service})
manager.SetRefreshTokenCfg(&manage.RefreshingConfig{
AccessTokenExp: accessTokenExpiry,
RefreshTokenExp: refreshTokenExpiry,
IsGenerateRefresh: refreshTokenExpiry > 0,
})
svr := server.NewDefaultServer(manager)
svr.SetUserAuthorizationHandler(func(w http.ResponseWriter, r *http.Request) (userID string, err error) {
auth, err := console.GetAuth(r.Context())
if err != nil {
return "", console.ErrUnauthorized.Wrap(err)
}
return auth.User.ID.String(), nil
})
// externalAddress _should_ end with a '/' suffix based on the calling path
return &Endpoint{
tokenStore: tokenStore,
service: service,
server: svr,
config: ProviderConfig{
Issuer: externalAddress,
AuthURL: externalAddress + "oauth/v2/authorize",
TokenURL: externalAddress + "oauth/v2/tokens",
UserInfoURL: externalAddress + "oauth/v2/userinfo",
},
}
}
// Endpoint implements an OpenID Connect (OIDC) Identity Provider. It grants client applications access to resources
// in the Storj network on behalf of the end user.
//
// architecture: Endpoint
type Endpoint struct {
tokenStore oauth2.TokenStore
service *console.Service
server *server.Server
config ProviderConfig
}
// WellKnownConfiguration renders the identity provider configuration that points clients to various endpoints.
func (e *Endpoint) WellKnownConfiguration(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
data, err := json.Marshal(e.config)
if err != nil {
http.Error(w, "", http.StatusInternalServerError)
} else {
http.ServeContent(w, r, "", time.Now(), bytes.NewReader(data))
}
}
// AuthorizeUser is called from an authenticated context granting the requester access to the application. We redirect
// back to the client application with the provided state and obtained code.
func (e *Endpoint) AuthorizeUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
err = e.server.HandleAuthorizeRequest(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
}
// Tokens exchanges unexpired refresh tokens or codes provided by AuthorizeUser for the associated set of tokens.
func (e *Endpoint) Tokens(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
err = e.server.HandleTokenRequest(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
}
// UserInfo uses the provided access token to look up the associated user information.
func (e *Endpoint) UserInfo(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
accessToken := r.Header.Get("Authorization")
if !strings.HasPrefix(accessToken, "Bearer ") {
http.Error(w, "", http.StatusUnauthorized)
return
}
accessToken = strings.TrimPrefix(accessToken, "Bearer ")
info, err := e.tokenStore.GetByAccess(ctx, accessToken)
if err != nil || info == nil {
http.Error(w, "", http.StatusUnauthorized)
return
}
userInfo, _, err := parseScope(info.GetScope())
if err != nil {
http.Error(w, "", http.StatusUnauthorized)
return
}
userID, err := uuid.FromString(info.GetUserID())
if err != nil {
http.Error(w, "", http.StatusUnauthorized)
return
}
user, err := e.service.GetUser(ctx, userID)
if err != nil {
http.Error(w, "", http.StatusUnauthorized)
return
}
if user.Status != console.Active {
http.Error(w, "", http.StatusUnauthorized)
return
}
userInfo.Subject = user.ID
userInfo.Email = user.Email
userInfo.EmailVerified = true
data, err := json.Marshal(userInfo)
if err != nil {
http.Error(w, "", http.StatusInternalServerError)
} else {
http.ServeContent(w, r, "", time.Now(), bytes.NewReader(data))
}
}
// ProviderConfig defines a subset of elements used by OIDC to auto-discover endpoints.
type ProviderConfig struct {
Issuer string `json:"issuer"`
AuthURL string `json:"authorization_endpoint"`
TokenURL string `json:"token_endpoint"`
UserInfoURL string `json:"userinfo_endpoint"`
}
// UserInfo provides a semi-standard object for common user information. The "cubbyhole" value is used to share the
// derived encryption key between client applications. In order to obtain it, the requesting client must decrypt
// the value using the key they provided when redirecting the user to login.
type UserInfo struct {
Subject uuid.UUID `json:"sub"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
// custom values below
Project string `json:"project"`
Buckets []string `json:"buckets"`
Cubbyhole string `json:"cubbyhole"`
}