Add OAuth2 API scope registration and enforcement
Register v1 API scopes in coredata, advertise them in OIDC discovery and protected-resource metadata, show them on the consent screen, and enforce scope-to-action mapping in the IAM Authorizer before policy evaluation. Signed-off-by: Ludovic Vielle <ludovic@probo.com>
This commit is contained in:
@@ -22,6 +22,7 @@ import (
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/bearertoken"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/iam/oauth2"
|
||||
)
|
||||
|
||||
func NewOAuth2AccessTokenMiddleware(svc *iam.Service) func(next http.Handler) http.Handler {
|
||||
@@ -53,6 +54,7 @@ func NewOAuth2AccessTokenMiddleware(svc *iam.Service) func(next http.Handler) ht
|
||||
}
|
||||
|
||||
ctx = ContextWithIdentity(ctx, identity)
|
||||
ctx = oauth2.ContextWithAccessToken(ctx, accessToken)
|
||||
|
||||
httpserver.LoggerFromContext(ctx).InfoCtx(
|
||||
ctx,
|
||||
|
||||
@@ -28,9 +28,9 @@ import (
|
||||
|
||||
type (
|
||||
AuthorizeFuncOption func(*iam.AuthorizeParams)
|
||||
AuthorizeFunc func(context.Context, gid.GID, string, ...AuthorizeFuncOption) (*coredata.Scope, error)
|
||||
AuthorizeFunc func(context.Context, gid.GID, iam.Action, ...AuthorizeFuncOption) (*coredata.Scope, error)
|
||||
BatchAuthorizeFuncOption func(*iam.AuthorizeBatchParams)
|
||||
BatchAuthorizeFunc func(context.Context, string, []gid.GID, ...BatchAuthorizeFuncOption) (*coredata.Scope, error)
|
||||
BatchAuthorizeFunc func(context.Context, iam.Action, []gid.GID, ...BatchAuthorizeFuncOption) (*coredata.Scope, error)
|
||||
)
|
||||
|
||||
func WithAttr(key, value string) AuthorizeFuncOption {
|
||||
@@ -78,7 +78,7 @@ func NewAuthorizeFunc(
|
||||
return func(
|
||||
ctx context.Context,
|
||||
objectID gid.GID,
|
||||
action string,
|
||||
action iam.Action,
|
||||
options ...AuthorizeFuncOption,
|
||||
) (*coredata.Scope, error) {
|
||||
identity := authn.IdentityFromContext(ctx)
|
||||
@@ -108,6 +108,10 @@ func NewAuthorizeFunc(
|
||||
return nil, gqlutils.Forbidden(ctx, err)
|
||||
}
|
||||
|
||||
if _, ok := errors.AsType[*iam.ErrInsufficientOAuth2Scope](err); ok {
|
||||
return nil, gqlutils.Forbidden(ctx, err)
|
||||
}
|
||||
|
||||
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return nil, gqlutils.NotFoundf(ctx, "resource not found")
|
||||
}
|
||||
@@ -127,7 +131,7 @@ func NewBatchAuthorizeFunc(
|
||||
) BatchAuthorizeFunc {
|
||||
return func(
|
||||
ctx context.Context,
|
||||
action string,
|
||||
action iam.Action,
|
||||
objectIDs []gid.GID,
|
||||
options ...BatchAuthorizeFuncOption,
|
||||
) (*coredata.Scope, error) {
|
||||
@@ -158,6 +162,10 @@ func NewBatchAuthorizeFunc(
|
||||
return nil, gqlutils.Forbidden(ctx, err)
|
||||
}
|
||||
|
||||
if _, ok := errors.AsType[*iam.ErrInsufficientOAuth2Scope](err); ok {
|
||||
return nil, gqlutils.Forbidden(ctx, err)
|
||||
}
|
||||
|
||||
if _, ok := errors.AsType[*iam.ErrMixedOrganizationBatch](err); ok {
|
||||
return nil, gqlutils.Invalid(ctx, err)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/iam/oauth2server"
|
||||
"go.probo.inc/probo/pkg/iam/oauth2"
|
||||
"go.probo.inc/probo/pkg/mail"
|
||||
"go.probo.inc/probo/pkg/server/api/authn"
|
||||
"go.probo.inc/probo/pkg/server/api/connect/v1/schema"
|
||||
@@ -176,7 +176,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
|
||||
return nil, gqlutils.NotFound(ctx, err)
|
||||
}
|
||||
|
||||
if oauthErr, ok := errors.AsType[*oauth2server.OAuth2Error](err); ok {
|
||||
if oauthErr, ok := errors.AsType[*oauth2.OAuth2Error](err); ok {
|
||||
return nil, gqlutils.Invalidf(ctx, "%s", oauthErr.Description())
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
|
||||
"go.gearno.de/kit/httpserver"
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/iam/oauth2server"
|
||||
"go.probo.inc/probo/pkg/iam/oauth2"
|
||||
"go.probo.inc/probo/pkg/server/api/connect/v1/types"
|
||||
)
|
||||
|
||||
@@ -35,13 +35,13 @@ func (h *OAuth2Handler) handleAuthorizeError(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
func (h *OAuth2Handler) renderOAuth2ErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
|
||||
oauthErr, ok := errors.AsType[*oauth2server.OAuth2Error](err)
|
||||
oauthErr, ok := errors.AsType[*oauth2.OAuth2Error](err)
|
||||
if !ok {
|
||||
httpserver.RenderError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Is(err, oauth2server.ErrServerError) {
|
||||
if errors.Is(err, oauth2.ErrServerError) {
|
||||
h.logger.ErrorCtx(r.Context(), "oauth2 server error", log.Error(err))
|
||||
}
|
||||
|
||||
@@ -54,15 +54,15 @@ func (h *OAuth2Handler) renderOAuth2ErrorResponse(w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
func isRedirectableError(err error) bool {
|
||||
return errors.Is(err, oauth2server.ErrAccessDenied) ||
|
||||
errors.Is(err, oauth2server.ErrInvalidRequest) ||
|
||||
errors.Is(err, oauth2server.ErrInvalidScope) ||
|
||||
errors.Is(err, oauth2server.ErrUnauthorizedClient) ||
|
||||
errors.Is(err, oauth2server.ErrInvalidGrant) ||
|
||||
errors.Is(err, oauth2server.ErrUnsupportedGrantType)
|
||||
return errors.Is(err, oauth2.ErrAccessDenied) ||
|
||||
errors.Is(err, oauth2.ErrInvalidRequest) ||
|
||||
errors.Is(err, oauth2.ErrInvalidScope) ||
|
||||
errors.Is(err, oauth2.ErrUnauthorizedClient) ||
|
||||
errors.Is(err, oauth2.ErrInvalidGrant) ||
|
||||
errors.Is(err, oauth2.ErrUnsupportedGrantType)
|
||||
}
|
||||
|
||||
func oauth2ErrorStatusCode(err *oauth2server.OAuth2Error) int {
|
||||
func oauth2ErrorStatusCode(err *oauth2.OAuth2Error) int {
|
||||
switch err.ErrorCode() {
|
||||
case "access_denied":
|
||||
return http.StatusForbidden
|
||||
@@ -75,22 +75,22 @@ func oauth2ErrorStatusCode(err *oauth2server.OAuth2Error) int {
|
||||
}
|
||||
}
|
||||
|
||||
func toOAuth2Error(err error) *oauth2server.OAuth2Error {
|
||||
func toOAuth2Error(err error) *oauth2.OAuth2Error {
|
||||
switch {
|
||||
case errors.Is(err, oauth2server.ErrClientNotFound):
|
||||
return oauth2server.NewError(oauth2server.ErrInvalidClient, oauth2server.WithDescription("client not found"))
|
||||
case errors.Is(err, oauth2server.ErrInvalidRedirectURI):
|
||||
return oauth2server.ErrInvalidRedirectURI
|
||||
case errors.Is(err, oauth2server.ErrUnauthorizedMember):
|
||||
return oauth2server.NewError(oauth2server.ErrUnauthorizedClient, oauth2server.WithDescription("client is private and user is not a member of the organization"))
|
||||
case errors.Is(err, oauth2server.ErrDeviceCodeNotPending):
|
||||
return oauth2server.NewError(oauth2server.ErrInvalidGrant, oauth2server.WithDescription("device code is not pending"))
|
||||
case errors.Is(err, oauth2.ErrClientNotFound):
|
||||
return oauth2.NewError(oauth2.ErrInvalidClient, oauth2.WithDescription("client not found"))
|
||||
case errors.Is(err, oauth2.ErrInvalidRedirectURI):
|
||||
return oauth2.ErrInvalidRedirectURI
|
||||
case errors.Is(err, oauth2.ErrUnauthorizedMember):
|
||||
return oauth2.NewError(oauth2.ErrUnauthorizedClient, oauth2.WithDescription("client is private and user is not a member of the organization"))
|
||||
case errors.Is(err, oauth2.ErrDeviceCodeNotPending):
|
||||
return oauth2.NewError(oauth2.ErrInvalidGrant, oauth2.WithDescription("device code is not pending"))
|
||||
default:
|
||||
if oauthErr, ok := errors.AsType[*oauth2server.OAuth2Error](err); ok {
|
||||
if oauthErr, ok := errors.AsType[*oauth2.OAuth2Error](err); ok {
|
||||
return oauthErr
|
||||
}
|
||||
|
||||
return oauth2server.NewError(oauth2server.ErrServerError, oauth2server.WithDescription("internal error"))
|
||||
return oauth2.NewError(oauth2.ErrServerError, oauth2.WithDescription("internal error"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ func redirectWithError(w http.ResponseWriter, r *http.Request, redirectURI, stat
|
||||
return
|
||||
}
|
||||
|
||||
oauthErr, ok := errors.AsType[*oauth2server.OAuth2Error](err)
|
||||
oauthErr, ok := errors.AsType[*oauth2.OAuth2Error](err)
|
||||
if !ok {
|
||||
httpserver.RenderError(w, http.StatusInternalServerError, errors.New("internal server error"))
|
||||
return
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
package connect_v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -30,18 +29,13 @@ import (
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/iam/oauth2server"
|
||||
"go.probo.inc/probo/pkg/iam/oauth2"
|
||||
"go.probo.inc/probo/pkg/securecookie"
|
||||
"go.probo.inc/probo/pkg/server/api/authn"
|
||||
"go.probo.inc/probo/pkg/server/api/connect/v1/types"
|
||||
"go.probo.inc/probo/pkg/uri"
|
||||
)
|
||||
|
||||
var (
|
||||
oauth2ClientContextKey = &ctxKey{name: "oauth2_client"}
|
||||
oauth2AccessTokenContextKey = &ctxKey{name: "oauth2_access_token"}
|
||||
)
|
||||
|
||||
type OAuth2Handler struct {
|
||||
iam *iam.Service
|
||||
sessionCookie *authn.Cookie
|
||||
@@ -63,29 +57,17 @@ func NewOAuth2Handler(
|
||||
}
|
||||
}
|
||||
|
||||
// oauth2ClientFromContext returns the authenticated OAuth2 client from context.
|
||||
func oauth2ClientFromContext(r *http.Request) *coredata.OAuth2Client {
|
||||
client, _ := r.Context().Value(oauth2ClientContextKey).(*coredata.OAuth2Client)
|
||||
return client
|
||||
}
|
||||
|
||||
// oauth2AccessTokenFromContext returns the validated OAuth2 access token from context.
|
||||
func oauth2AccessTokenFromContext(r *http.Request) *coredata.OAuth2AccessToken {
|
||||
token, _ := r.Context().Value(oauth2AccessTokenContextKey).(*coredata.OAuth2AccessToken)
|
||||
return token
|
||||
}
|
||||
|
||||
// ClientAuthMiddleware authenticates the OAuth2 client from HTTP Basic auth
|
||||
// or POST body credentials and stores it in the request context.
|
||||
func (h *OAuth2Handler) ClientAuthMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
client, err := h.authenticateClient(r)
|
||||
if err != nil {
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2server.ErrInvalidClient)
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2.ErrInvalidClient)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), oauth2ClientContextKey, client)
|
||||
ctx := oauth2.ContextWithClient(r.Context(), client)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
@@ -110,15 +92,15 @@ func (h *OAuth2Handler) BearerTokenMiddleware(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), oauth2AccessTokenContextKey, accessToken)
|
||||
ctx := oauth2.ContextWithAccessToken(r.Context(), accessToken)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func (h *OAuth2Handler) endpoints() oauth2server.Endpoints {
|
||||
func (h *OAuth2Handler) endpoints() oauth2.Endpoints {
|
||||
api := h.baseURL.String() + "/api/connect/v1"
|
||||
|
||||
return oauth2server.Endpoints{
|
||||
return oauth2.Endpoints{
|
||||
Authorization: uri.URI(api + "/oauth2/authorize"),
|
||||
Token: uri.URI(api + "/oauth2/token"),
|
||||
Userinfo: uri.URI(api + "/oauth2/userinfo"),
|
||||
@@ -135,7 +117,7 @@ func (h *OAuth2Handler) endpoints() oauth2server.Endpoints {
|
||||
// DiscoveryHandler serves the OpenID Connect Discovery document.
|
||||
// GET /.well-known/openid-configuration
|
||||
func (h *OAuth2Handler) DiscoveryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
metadata := h.iam.OAuth2ServerService.Metadata(h.endpoints())
|
||||
metadata := h.iam.OAuth2ServerMetadata(h.endpoints())
|
||||
|
||||
PublicCache(w, 1*time.Hour)
|
||||
httpserver.RenderJSON(w, http.StatusOK, metadata)
|
||||
@@ -168,7 +150,7 @@ func (h *OAuth2Handler) AuthorizeHandler(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
var in types.OAuth2AuthorizeInput
|
||||
if err := in.DecodeQuery(r.URL.Query()); err != nil {
|
||||
h.handleAuthorizeError(w, r, oauth2server.NewError(oauth2server.ErrInvalidRequest, oauth2server.WithError(err)), "", "")
|
||||
h.handleAuthorizeError(w, r, oauth2.NewError(oauth2.ErrInvalidRequest, oauth2.WithError(err)), "", "")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -181,7 +163,7 @@ func (h *OAuth2Handler) AuthorizeHandler(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
code, err := h.iam.OAuth2ServerService.Authorize(
|
||||
r.Context(),
|
||||
&oauth2server.AuthorizeRequest{
|
||||
&oauth2.AuthorizeRequest{
|
||||
IdentityID: identity.ID,
|
||||
SessionID: session.ID,
|
||||
ResponseType: in.ResponseType,
|
||||
@@ -196,7 +178,7 @@ func (h *OAuth2Handler) AuthorizeHandler(w http.ResponseWriter, r *http.Request)
|
||||
},
|
||||
)
|
||||
|
||||
if consentErr, ok := errors.AsType[*oauth2server.ConsentRequiredError](err); ok {
|
||||
if consentErr, ok := errors.AsType[*oauth2.ConsentRequiredError](err); ok {
|
||||
consentURL := h.baseURL.WithPath("/auth/consent").
|
||||
WithQuery("consent_id", consentErr.ConsentID.String()).
|
||||
MustString()
|
||||
@@ -217,7 +199,7 @@ func (h *OAuth2Handler) AuthorizeHandler(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
func (h *OAuth2Handler) TokenHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2server.NewError(oauth2server.ErrInvalidRequest, oauth2server.WithDescription("invalid form data")))
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2.NewError(oauth2.ErrInvalidRequest, oauth2.WithDescription("invalid form data")))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -227,7 +209,7 @@ func (h *OAuth2Handler) TokenHandler(w http.ResponseWriter, r *http.Request) {
|
||||
)
|
||||
|
||||
if err := grantType.UnmarshalText([]byte(value)); err != nil {
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2server.ErrUnsupportedGrantType)
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2.ErrUnsupportedGrantType)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -244,13 +226,15 @@ func (h *OAuth2Handler) TokenHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *OAuth2Handler) IntrospectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
client = oauth2ClientFromContext(r)
|
||||
in = types.OAuth2IntrospectInput{}
|
||||
)
|
||||
client, ok := oauth2.ClientFromContext(r.Context())
|
||||
if !ok {
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2.ErrInvalidClient)
|
||||
return
|
||||
}
|
||||
|
||||
in := types.OAuth2IntrospectInput{}
|
||||
if err := in.DecodeForm(r); err != nil {
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2server.NewError(oauth2server.ErrInvalidRequest, oauth2server.WithError(err)))
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2.NewError(oauth2.ErrInvalidRequest, oauth2.WithError(err)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -270,12 +254,12 @@ func (h *OAuth2Handler) IntrospectHandler(w http.ResponseWriter, r *http.Request
|
||||
|
||||
func (h *OAuth2Handler) RevokeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
client = oauth2ClientFromContext(r)
|
||||
in = types.OAuth2RevokeInput{}
|
||||
client, _ = oauth2.ClientFromContext(r.Context())
|
||||
in = types.OAuth2RevokeInput{}
|
||||
)
|
||||
|
||||
if err := in.DecodeForm(r); err != nil {
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2server.NewError(oauth2server.ErrInvalidRequest, oauth2server.WithError(err)))
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2.NewError(oauth2.ErrInvalidRequest, oauth2.WithError(err)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -300,7 +284,7 @@ func (h *OAuth2Handler) RevokeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *OAuth2Handler) DeviceAuthHandler(w http.ResponseWriter, r *http.Request) {
|
||||
in := types.OAuth2DeviceAuthInput{}
|
||||
if err := in.DecodeForm(r); err != nil {
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2server.NewError(oauth2server.ErrInvalidRequest, oauth2server.WithError(err)))
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2.NewError(oauth2.ErrInvalidRequest, oauth2.WithError(err)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -345,7 +329,7 @@ func (h *OAuth2Handler) RegisterHandler(w http.ResponseWriter, r *http.Request)
|
||||
h.renderOAuth2ErrorResponse(
|
||||
w,
|
||||
r,
|
||||
oauth2server.NewError(oauth2server.ErrInvalidRequest, oauth2server.WithDescription("invalid JSON body")),
|
||||
oauth2.NewError(oauth2.ErrInvalidRequest, oauth2.WithDescription("invalid JSON body")),
|
||||
)
|
||||
|
||||
return
|
||||
@@ -369,15 +353,15 @@ func (h *OAuth2Handler) RegisterHandler(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
if len(in.Scopes) == 0 {
|
||||
in.Scopes = coredata.OAuth2Scopes{
|
||||
coredata.OAuth2ScopeOpenID,
|
||||
coredata.OAuth2ScopeProfile,
|
||||
coredata.OAuth2ScopeEmail,
|
||||
oauth2.ScopeOpenID,
|
||||
oauth2.ScopeProfile,
|
||||
oauth2.ScopeEmail,
|
||||
}
|
||||
}
|
||||
|
||||
clientID, clientSecret, err := h.iam.OAuth2ServerService.RegisterClient(
|
||||
r.Context(),
|
||||
&oauth2server.RegisterClientRequest{
|
||||
&oauth2.RegisterClientRequest{
|
||||
IdentityID: identity.ID,
|
||||
OrganizationID: in.OrganizationID,
|
||||
ClientName: in.ClientName,
|
||||
@@ -417,7 +401,13 @@ func (h *OAuth2Handler) RegisterHandler(w http.ResponseWriter, r *http.Request)
|
||||
// UserInfoHandler serves the OIDC UserInfo endpoint.
|
||||
// GET /oauth2/userinfo
|
||||
func (h *OAuth2Handler) UserInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
accessToken := oauth2AccessTokenFromContext(r)
|
||||
accessToken, ok := oauth2.AccessTokenFromContext(r.Context())
|
||||
if !ok {
|
||||
w.Header().Set("WWW-Authenticate", `Bearer error="invalid_token"`)
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := h.iam.OAuth2ServerService.UserInfo(
|
||||
r.Context(),
|
||||
@@ -425,7 +415,7 @@ func (h *OAuth2Handler) UserInfoHandler(w http.ResponseWriter, r *http.Request)
|
||||
accessToken.Scopes,
|
||||
)
|
||||
if err != nil {
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2server.ErrServerError)
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2.ErrServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -452,12 +442,12 @@ func (h *OAuth2Handler) authenticateClient(r *http.Request) (*coredata.OAuth2Cli
|
||||
}
|
||||
|
||||
if clientIDStr == "" {
|
||||
return nil, oauth2server.ErrInvalidClient
|
||||
return nil, oauth2.ErrInvalidClient
|
||||
}
|
||||
|
||||
clientID, err := gid.ParseGID(clientIDStr)
|
||||
if err != nil {
|
||||
return nil, oauth2server.ErrInvalidClient
|
||||
return nil, oauth2.ErrInvalidClient
|
||||
}
|
||||
|
||||
return h.iam.OAuth2ServerService.AuthenticateClient(r.Context(), clientID, clientSecret)
|
||||
@@ -466,13 +456,13 @@ func (h *OAuth2Handler) authenticateClient(r *http.Request) (*coredata.OAuth2Cli
|
||||
func (h *OAuth2Handler) handleAuthorizationCodeGrant(w http.ResponseWriter, r *http.Request) {
|
||||
client, err := h.authenticateClient(r)
|
||||
if err != nil {
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2server.ErrInvalidClient)
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2.ErrInvalidClient)
|
||||
return
|
||||
}
|
||||
|
||||
var in types.OAuth2AuthorizationCodeGrantInput
|
||||
if err := in.DecodeForm(r); err != nil {
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2server.NewError(oauth2server.ErrInvalidGrant, oauth2server.WithError(err)))
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2.NewError(oauth2.ErrInvalidGrant, oauth2.WithError(err)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -484,7 +474,7 @@ func (h *OAuth2Handler) handleAuthorizationCodeGrant(w http.ResponseWriter, r *h
|
||||
in.CodeVerifier,
|
||||
)
|
||||
if err != nil {
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2server.NewError(oauth2server.ErrInvalidGrant, oauth2server.WithDescription("invalid or expired code")))
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2.NewError(oauth2.ErrInvalidGrant, oauth2.WithDescription("invalid or expired code")))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -495,19 +485,19 @@ func (h *OAuth2Handler) handleAuthorizationCodeGrant(w http.ResponseWriter, r *h
|
||||
func (h *OAuth2Handler) handleRefreshTokenGrant(w http.ResponseWriter, r *http.Request) {
|
||||
client, err := h.authenticateClient(r)
|
||||
if err != nil {
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2server.ErrInvalidClient)
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2.ErrInvalidClient)
|
||||
return
|
||||
}
|
||||
|
||||
var in types.OAuth2RefreshTokenGrantInput
|
||||
if err := in.DecodeForm(r); err != nil {
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2server.NewError(oauth2server.ErrInvalidGrant, oauth2server.WithError(err)))
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2.NewError(oauth2.ErrInvalidGrant, oauth2.WithError(err)))
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.iam.OAuth2ServerService.RefreshToken(r.Context(), client, in.RefreshToken)
|
||||
if err != nil {
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2server.NewError(oauth2server.ErrInvalidGrant, oauth2server.WithDescription("invalid or expired refresh token")))
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2.NewError(oauth2.ErrInvalidGrant, oauth2.WithDescription("invalid or expired refresh token")))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -518,7 +508,7 @@ func (h *OAuth2Handler) handleRefreshTokenGrant(w http.ResponseWriter, r *http.R
|
||||
func (h *OAuth2Handler) handleDeviceCodeGrant(w http.ResponseWriter, r *http.Request) {
|
||||
var in types.OAuth2DeviceCodeGrantInput
|
||||
if err := in.DecodeForm(r); err != nil {
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2server.NewError(oauth2server.ErrInvalidRequest, oauth2server.WithError(err)))
|
||||
h.renderOAuth2ErrorResponse(w, r, oauth2.NewError(oauth2.ErrInvalidRequest, oauth2.WithError(err)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -536,7 +526,7 @@ func (h *OAuth2Handler) handleDeviceCodeGrant(w http.ResponseWriter, r *http.Req
|
||||
httpserver.RenderJSON(w, http.StatusOK, tokenResultToResponse(result))
|
||||
}
|
||||
|
||||
func tokenResultToResponse(r *oauth2server.TokenResult) *types.OAuth2TokenResponse {
|
||||
func tokenResultToResponse(r *oauth2.TokenResult) *types.OAuth2TokenResponse {
|
||||
return &types.OAuth2TokenResponse{
|
||||
AccessToken: r.AccessToken,
|
||||
TokenType: r.TokenType,
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/iam/oauth2server"
|
||||
"go.probo.inc/probo/pkg/iam/oauth2"
|
||||
"go.probo.inc/probo/pkg/server/api/authn"
|
||||
"go.probo.inc/probo/pkg/server/api/connect/v1/schema"
|
||||
"go.probo.inc/probo/pkg/server/api/connect/v1/types"
|
||||
@@ -39,13 +39,13 @@ func (r *mutationResolver) AuthorizeDevice(ctx context.Context, input types.Auth
|
||||
|
||||
err := r.iam.OAuth2ServerService.AuthorizeDevice(ctx, identity.ID, session.ID, userCode)
|
||||
if err != nil {
|
||||
if consentErr, ok := errors.AsType[*oauth2server.ConsentRequiredError](err); ok {
|
||||
if consentErr, ok := errors.AsType[*oauth2.ConsentRequiredError](err); ok {
|
||||
return &types.AuthorizeDevicePayload{
|
||||
ConsentID: &consentErr.ConsentID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if oauthErr, ok := errors.AsType[*oauth2server.OAuth2Error](err); ok {
|
||||
if oauthErr, ok := errors.AsType[*oauth2.OAuth2Error](err); ok {
|
||||
return nil, gqlutils.Invalidf(ctx, "%s", oauthErr.Description())
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func (r *mutationResolver) ApproveConsent(ctx context.Context, input types.Appro
|
||||
|
||||
result, err := r.iam.OAuth2ServerService.ApproveConsent(
|
||||
ctx,
|
||||
&oauth2server.ConsentApprovalRequest{
|
||||
&oauth2.ConsentApprovalRequest{
|
||||
ConsentID: input.ConsentID,
|
||||
IdentityID: identity.ID,
|
||||
SessionID: session.ID,
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/iam/oauth2server"
|
||||
"go.probo.inc/probo/pkg/iam/oauth2"
|
||||
"go.probo.inc/probo/pkg/uri"
|
||||
)
|
||||
|
||||
@@ -328,7 +328,7 @@ func InactiveIntrospectResponse() *OAuth2IntrospectResponse {
|
||||
return &OAuth2IntrospectResponse{Active: false}
|
||||
}
|
||||
|
||||
func ActiveIntrospectResponse(result *oauth2server.IntrospectResult) *OAuth2IntrospectResponse {
|
||||
func ActiveIntrospectResponse(result *oauth2.IntrospectResult) *OAuth2IntrospectResponse {
|
||||
return &OAuth2IntrospectResponse{
|
||||
Active: true,
|
||||
Scope: result.Scopes,
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"go.probo.inc/probo/pkg/agentrun"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/probo"
|
||||
"go.probo.inc/probo/pkg/server/api/authn"
|
||||
"go.probo.inc/probo/pkg/server/api/console/v1/schema"
|
||||
@@ -33,7 +32,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
|
||||
|
||||
switch id.EntityType() {
|
||||
case coredata.OrganizationEntityType:
|
||||
action = iam.ActionOrganizationGet
|
||||
action = probo.ActionOrganizationGet
|
||||
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
|
||||
organization, err := r.probo.Organizations.Get(ctx, scope, id)
|
||||
if err != nil {
|
||||
@@ -547,7 +546,7 @@ func (r *queryResolver) CommonThirdParties(ctx context.Context, name string) ([]
|
||||
func (r *queryResolver) AccessReviewDrivers(ctx context.Context) ([]*types.ConnectorProviderInfo, error) {
|
||||
identity := authn.IdentityFromContext(ctx)
|
||||
|
||||
if _, err := r.authorize(ctx, identity.ID, probo.ActionAccessReviewDriverCatalogList); err != nil {
|
||||
if _, err := r.authorize(ctx, identity.ID, accessreview.ActionDriverCatalogList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
"go.probo.inc/probo/pkg/connector"
|
||||
)
|
||||
|
||||
// oauthClientMetadata is the OAuth Client ID Metadata Document (CIMD)
|
||||
// oauth2ClientMetadata is the OAuth2 Client ID Metadata Document (CIMD)
|
||||
// published for public-client connectors. The deployment's
|
||||
// (baseURL + CIMDMetadataPath) URL is the OAuth client_id; providers such as
|
||||
// PostHog fetch this document server-to-server during authorization to learn
|
||||
@@ -37,7 +37,7 @@ const (
|
||||
proboLogoURI = "https://www.probo.com/probo-logo-only.svg"
|
||||
)
|
||||
|
||||
type oauthClientMetadata struct {
|
||||
type oauth2ClientMetadata struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientName string `json:"client_name"`
|
||||
ClientURI string `json:"client_uri"`
|
||||
@@ -48,11 +48,11 @@ type oauthClientMetadata struct {
|
||||
ResponseTypes []string `json:"response_types"`
|
||||
}
|
||||
|
||||
// handleConnectorOAuthClientMetadata serves the public, unauthenticated CIMD
|
||||
// document. It is intentionally outside the auth middleware group: the OAuth
|
||||
// handleConnectorOAuth2ClientMetadata serves the public, unauthenticated CIMD
|
||||
// document. It is intentionally outside the auth middleware group: the OAuth2
|
||||
// provider fetches it without any Probo credentials.
|
||||
func handleConnectorOAuthClientMetadata(baseURL *baseurl.BaseURL) http.HandlerFunc {
|
||||
doc := oauthClientMetadata{
|
||||
func handleConnectorOAuth2ClientMetadata(baseURL *baseurl.BaseURL) http.HandlerFunc {
|
||||
doc := oauth2ClientMetadata{
|
||||
ClientID: baseURL.WithPath(connector.CIMDMetadataPath).MustString(),
|
||||
ClientName: "Probo",
|
||||
ClientURI: proboBrandURI,
|
||||
@@ -25,18 +25,18 @@ import (
|
||||
"go.probo.inc/probo/pkg/baseurl"
|
||||
)
|
||||
|
||||
// TestHandleConnectorOAuthClientMetadata verifies the public CIMD document:
|
||||
// TestHandleConnectorOAuth2ClientMetadata verifies the public CIMD document:
|
||||
// PostHog fetches it server-to-server during authorization, so client_id,
|
||||
// redirect_uris (derived from the deployment base URL) and the public-client
|
||||
// token_endpoint_auth_method must be exactly right or the OAuth flow breaks.
|
||||
func TestHandleConnectorOAuthClientMetadata(t *testing.T) {
|
||||
func TestHandleConnectorOAuth2ClientMetadata(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
base, err := baseurl.Parse("https://probo.example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handleConnectorOAuthClientMetadata(base)(
|
||||
handleConnectorOAuth2ClientMetadata(base)(
|
||||
rec,
|
||||
httptest.NewRequest(http.MethodGet, "/api/console/v1/connectors/oauth-client-metadata", nil),
|
||||
)
|
||||
@@ -35,7 +35,7 @@ func NewAuthorizeFunc(logger *log.Logger) authz.AuthorizeFunc {
|
||||
return func(
|
||||
ctx context.Context,
|
||||
objectID gid.GID,
|
||||
action string,
|
||||
action iam.Action,
|
||||
options ...authz.AuthorizeFuncOption,
|
||||
) (*coredata.Scope, error) {
|
||||
loaders := FromContext(ctx)
|
||||
@@ -66,6 +66,10 @@ func NewAuthorizeFunc(logger *log.Logger) authz.AuthorizeFunc {
|
||||
return nil, gqlutils.Forbidden(ctx, err)
|
||||
}
|
||||
|
||||
if _, ok := errors.AsType[*iam.ErrInsufficientOAuth2Scope](err); ok {
|
||||
return nil, gqlutils.Forbidden(ctx, err)
|
||||
}
|
||||
|
||||
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return nil, gqlutils.NotFoundf(ctx, "resource not found")
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ type (
|
||||
// batch together.
|
||||
AuthorizeKey struct {
|
||||
ResourceID gid.GID
|
||||
Action string
|
||||
Action iam.Action
|
||||
ResourceAttributes string
|
||||
DryRun bool
|
||||
SkipAssumptionCheck bool
|
||||
|
||||
@@ -142,7 +142,7 @@ func NewMux(
|
||||
// is fetched server-to-server by public-client providers (PostHog)
|
||||
// during authorization, with no Probo credentials. Mounted outside the
|
||||
// auth group above.
|
||||
r.Get("/connectors/oauth-client-metadata", handleConnectorOAuthClientMetadata(baseURL))
|
||||
r.Get("/connectors/oauth-client-metadata", handleConnectorOAuth2ClientMetadata(baseURL))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -157,7 +157,18 @@ func (h *Handler) handleGetFile(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
scope, err := h.iamSvc.Authorizer.Authorize(ctx, params)
|
||||
if err != nil {
|
||||
if _, ok := errors.AsType[*iam.ErrInsufficientOAuth2Scope](err); ok {
|
||||
jsonx.RenderForbidden(w)
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := errors.AsType[*iam.ErrInsufficientPermissions](err); ok {
|
||||
jsonx.RenderForbidden(w)
|
||||
return
|
||||
}
|
||||
|
||||
jsonx.RenderNotFound(w, fmt.Errorf("file not found"))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,10 @@ func (r *Resolver) Authorize(ctx context.Context, entityID gid.GID, action iam.A
|
||||
return nil, fmt.Errorf("permission denied")
|
||||
}
|
||||
|
||||
if _, ok := errors.AsType[*iam.ErrInsufficientOAuth2Scope](err); ok {
|
||||
return nil, fmt.Errorf("insufficient scope")
|
||||
}
|
||||
|
||||
if _, ok := errors.AsType[*iam.ErrAssumptionRequired](err); ok {
|
||||
return nil, fmt.Errorf("assumption required")
|
||||
}
|
||||
@@ -114,6 +118,10 @@ func (r *Resolver) AuthorizeBatch(ctx context.Context, entityIDs []gid.GID, acti
|
||||
return nil, fmt.Errorf("permission denied")
|
||||
}
|
||||
|
||||
if _, ok := errors.AsType[*iam.ErrInsufficientOAuth2Scope](err); ok {
|
||||
return nil, fmt.Errorf("insufficient scope")
|
||||
}
|
||||
|
||||
if _, ok := errors.AsType[*iam.ErrAssumptionRequired](err); ok {
|
||||
return nil, fmt.Errorf("assumption required")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user