4a110b266e
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
199 lines
5.5 KiB
Go
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"`
|
|
}
|