2022-02-03 20:49:38 +00:00
|
|
|
// Copyright (C) 2022 Storj Labs, Inc.
|
|
|
|
// See LICENSE for copying information.
|
|
|
|
|
|
|
|
package oidc
|
|
|
|
|
|
|
|
import (
|
2022-02-14 20:06:35 +00:00
|
|
|
"database/sql"
|
2022-02-03 20:49:38 +00:00
|
|
|
"encoding/json"
|
2022-02-14 20:06:35 +00:00
|
|
|
"errors"
|
2022-02-03 20:49:38 +00:00
|
|
|
"net/http"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/go-oauth2/oauth2/v4"
|
|
|
|
"github.com/go-oauth2/oauth2/v4/manage"
|
|
|
|
"github.com/go-oauth2/oauth2/v4/server"
|
2022-02-14 20:06:35 +00:00
|
|
|
"github.com/gorilla/mux"
|
2022-02-03 20:49:38 +00:00
|
|
|
"github.com/spacemonkeygo/monkit/v3"
|
2022-02-08 21:28:11 +00:00
|
|
|
"go.uber.org/zap"
|
2022-02-03 20:49:38 +00:00
|
|
|
|
2022-09-12 17:23:36 +01:00
|
|
|
"storj.io/common/storj"
|
2022-02-03 20:49:38 +00:00
|
|
|
"storj.io/common/uuid"
|
|
|
|
"storj.io/storj/satellite/console"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
mon = monkit.Package()
|
|
|
|
)
|
|
|
|
|
|
|
|
// NewEndpoint constructs an OpenID identity provider.
|
2022-02-08 21:28:11 +00:00
|
|
|
func NewEndpoint(
|
2022-09-12 17:23:36 +01:00
|
|
|
nodeURL storj.NodeURL, externalAddress string, log *zap.Logger,
|
2022-02-08 21:28:11 +00:00
|
|
|
oidcService *Service, service *console.Service,
|
|
|
|
codeExpiry, accessTokenExpiry, refreshTokenExpiry time.Duration,
|
|
|
|
) *Endpoint {
|
2022-02-03 20:49:38 +00:00
|
|
|
manager := manage.NewManager()
|
|
|
|
|
2022-02-14 20:06:35 +00:00
|
|
|
clientStore := oidcService.ClientStore()
|
2022-02-03 20:49:38 +00:00
|
|
|
tokenStore := oidcService.TokenStore()
|
|
|
|
|
2022-02-14 20:06:35 +00:00
|
|
|
manager.MapClientStorage(clientStore)
|
2022-02-03 20:49:38 +00:00
|
|
|
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) {
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := console.GetUser(r.Context())
|
2022-02-03 20:49:38 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", console.ErrUnauthorized.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
return user.ID.String(), nil
|
2022-02-03 20:49:38 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
// externalAddress _should_ end with a '/' suffix based on the calling path
|
|
|
|
return &Endpoint{
|
2022-02-14 20:06:35 +00:00
|
|
|
clientStore: clientStore,
|
|
|
|
tokenStore: tokenStore,
|
|
|
|
service: service,
|
|
|
|
server: svr,
|
|
|
|
log: log,
|
2022-02-03 20:49:38 +00:00
|
|
|
config: ProviderConfig{
|
2022-09-12 17:23:36 +01:00
|
|
|
NodeURL: nodeURL.String(),
|
2022-02-03 20:49:38 +00:00
|
|
|
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 {
|
2022-02-14 20:06:35 +00:00
|
|
|
clientStore oauth2.ClientStore
|
|
|
|
tokenStore oauth2.TokenStore
|
|
|
|
service *console.Service
|
|
|
|
server *server.Server
|
|
|
|
log *zap.Logger
|
|
|
|
config ProviderConfig
|
2022-02-03 20:49:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
2022-02-08 21:28:11 +00:00
|
|
|
w.Header().Set("Content-Type", "application/json")
|
2022-02-03 20:49:38 +00:00
|
|
|
|
2022-02-08 21:28:11 +00:00
|
|
|
err = json.NewEncoder(w).Encode(e.config)
|
2022-02-03 20:49:38 +00:00
|
|
|
if err != nil {
|
2022-02-08 21:28:11 +00:00
|
|
|
e.log.Error("failed to encode oidc config", zap.Error(err))
|
2022-02-03 20:49:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
2022-02-08 21:28:11 +00:00
|
|
|
e.log.Error("failed to authorize user", zap.Error(err))
|
2022-02-03 20:49:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
2022-02-08 21:28:11 +00:00
|
|
|
e.log.Error("failed to exchange for token", zap.Error(err))
|
2022-02-03 20:49:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
2022-02-08 21:28:11 +00:00
|
|
|
w.Header().Set("Content-Type", "application/json")
|
2022-02-03 20:49:38 +00:00
|
|
|
|
2022-02-08 21:28:11 +00:00
|
|
|
err = json.NewEncoder(w).Encode(userInfo)
|
2022-02-03 20:49:38 +00:00
|
|
|
if err != nil {
|
2022-02-08 21:28:11 +00:00
|
|
|
e.log.Error("failed to encode user info", zap.Error(err))
|
2022-02-03 20:49:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-14 20:06:35 +00:00
|
|
|
// GetClient returns non-sensitive information about an OAuthClient. This information is used to initially verify client
|
|
|
|
// applications who are requesting information on behalf of a user.
|
|
|
|
func (e *Endpoint) GetClient(w http.ResponseWriter, r *http.Request) {
|
|
|
|
ctx := r.Context()
|
|
|
|
var err error
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
vars := mux.Vars(r)
|
|
|
|
|
|
|
|
client, err := e.clientStore.GetByID(ctx, vars["id"])
|
|
|
|
switch {
|
|
|
|
case errors.Is(err, sql.ErrNoRows):
|
|
|
|
http.NotFound(w, r)
|
|
|
|
return
|
|
|
|
case err != nil:
|
|
|
|
http.Error(w, "", http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
|
|
|
err = json.NewEncoder(w).Encode(client)
|
|
|
|
if err != nil {
|
|
|
|
e.log.Error("failed to encode oauth client", zap.Error(err))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-03 20:49:38 +00:00
|
|
|
// ProviderConfig defines a subset of elements used by OIDC to auto-discover endpoints.
|
|
|
|
type ProviderConfig struct {
|
2022-09-12 17:23:36 +01:00
|
|
|
NodeURL string `json:"node_url"`
|
2022-02-03 20:49:38 +00:00
|
|
|
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"`
|
|
|
|
}
|