diff --git a/authenticate/handlers.go b/authenticate/handlers.go index f9fb15ed3..894742760 100644 --- a/authenticate/handlers.go +++ b/authenticate/handlers.go @@ -568,6 +568,8 @@ func (a *Authenticate) getUserInfoData(r *http.Request) (handlers.UserInfoData, WebAuthnCreationOptions: creationOptions, WebAuthnRequestOptions: requestOptions, WebAuthnURL: urlutil.WebAuthnURL(r, authenticateURL, state.sharedKey, r.URL.Query()), + + BrandingOptions: a.options.Load().BrandingOptions, }, nil } diff --git a/authenticate/handlers/userinfo.go b/authenticate/handlers/userinfo.go index 99d227076..015512afe 100644 --- a/authenticate/handlers/userinfo.go +++ b/authenticate/handlers/userinfo.go @@ -26,11 +26,13 @@ type UserInfoData struct { WebAuthnCreationOptions *webauthn.PublicKeyCredentialCreationOptions WebAuthnRequestOptions *webauthn.PublicKeyCredentialRequestOptions WebAuthnURL string + + BrandingOptions httputil.BrandingOptions } // ToJSON converts the data into a JSON map. -func (data UserInfoData) ToJSON() map[string]interface{} { - m := map[string]interface{}{} +func (data UserInfoData) ToJSON() map[string]any { + m := map[string]any{} m["csrfToken"] = data.CSRFToken var directoryGroups []json.RawMessage for _, directoryGroup := range data.DirectoryGroups { @@ -52,6 +54,7 @@ func (data UserInfoData) ToJSON() map[string]interface{} { m["webAuthnCreationOptions"] = data.WebAuthnCreationOptions m["webAuthnRequestOptions"] = data.WebAuthnRequestOptions m["webAuthnUrl"] = data.WebAuthnURL + httputil.AddBrandingOptionsToMap(m, data.BrandingOptions) return m } diff --git a/authorize/check_response.go b/authorize/check_response.go index 233dfc7b7..e3d79964f 100644 --- a/authorize/check_response.go +++ b/authorize/check_response.go @@ -107,10 +107,11 @@ func (a *Authorize) deniedResponse( // run the request through our go error handler httpErr := httputil.HTTPError{ - Status: int(code), - Err: errors.New(reason), - DebugURL: debugEndpoint, - RequestID: requestid.FromContext(ctx), + Status: int(code), + Err: errors.New(reason), + DebugURL: debugEndpoint, + RequestID: requestid.FromContext(ctx), + BrandingOptions: a.currentOptions.Load().BrandingOptions, } httpErr.ErrorResponse(ctx, w, r) diff --git a/config/options.go b/config/options.go index 76f268ca2..eca55425e 100644 --- a/config/options.go +++ b/config/options.go @@ -26,6 +26,7 @@ import ( "github.com/pomerium/pomerium/internal/directory/okta" "github.com/pomerium/pomerium/internal/directory/onelogin" "github.com/pomerium/pomerium/internal/hashutil" + "github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/identity/oauth" "github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/sets" @@ -298,6 +299,8 @@ type Options struct { CodecType CodecType `mapstructure:"codec_type" yaml:"codec_type"` AuditKey *PublicKeyEncryptionKeyOptions `mapstructure:"audit_key"` + + BrandingOptions httputil.BrandingOptions } type certificateFilePair struct { @@ -1544,6 +1547,7 @@ func (o *Options) ApplySettings(ctx context.Context, settings *config.Settings) if settings.ClientCrlFile != nil { o.ClientCRLFile = settings.GetClientCrlFile() } + o.BrandingOptions = settings } func dataDir() string { diff --git a/internal/httputil/branding.go b/internal/httputil/branding.go new file mode 100644 index 000000000..f89b50311 --- /dev/null +++ b/internal/httputil/branding.go @@ -0,0 +1,35 @@ +package httputil + +// The BrandingOptions customize the user info and error pages. +type BrandingOptions interface { + GetPrimaryColor() string + GetSecondaryColor() string + GetDarkmodePrimaryColor() string + GetDarkmodeSecondaryColor() string + GetLogoUrl() string + GetFaviconUrl() string + GetErrorMessageFirstParagraph() string +} + +// AddBrandingOptionsToMap adds the branding options to the map. +func AddBrandingOptionsToMap(dst map[string]any, brandingOptions BrandingOptions) { + if brandingOptions == nil { + return + } + + if brandingOptions.GetPrimaryColor() != "" { + dst["primaryColor"] = brandingOptions.GetPrimaryColor() + } + if brandingOptions.GetSecondaryColor() != "" { + dst["secondaryColor"] = brandingOptions.GetSecondaryColor() + } + if brandingOptions.GetLogoUrl() != "" { + dst["logoUrl"] = brandingOptions.GetLogoUrl() + } + if brandingOptions.GetFaviconUrl() != "" { + dst["faviconUrl"] = brandingOptions.GetFaviconUrl() + } + if brandingOptions.GetErrorMessageFirstParagraph() != "" { + dst["errorMessageFirstParagraph"] = brandingOptions.GetErrorMessageFirstParagraph() + } +} diff --git a/internal/httputil/errors.go b/internal/httputil/errors.go index 2dea9e34b..6ad70099b 100644 --- a/internal/httputil/errors.go +++ b/internal/httputil/errors.go @@ -20,6 +20,8 @@ type HTTPError struct { DebugURL *url.URL // The request ID. RequestID string + + BrandingOptions BrandingOptions } // NewError returns an error that contains a HTTP status and error. @@ -73,7 +75,7 @@ func (e *HTTPError) ErrorResponse(ctx context.Context, w http.ResponseWriter, r return } - m := map[string]interface{}{ + m := map[string]any{ "canDebug": response.CanDebug, "error": response.Error, "requestId": response.RequestID, @@ -84,6 +86,7 @@ func (e *HTTPError) ErrorResponse(ctx context.Context, w http.ResponseWriter, r if response.DebugURL != nil { m["debugUrl"] = response.DebugURL.String() } + AddBrandingOptionsToMap(m, e.BrandingOptions) w.Header().Set("Content-Type", "text/html; charset=UTF-8") w.WriteHeader(response.Status) diff --git a/pkg/grpc/config/config.pb.go b/pkg/grpc/config/config.pb.go index 9d7c6377f..d64ad11f1 100644 --- a/pkg/grpc/config/config.pb.go +++ b/pkg/grpc/config/config.pb.go @@ -991,6 +991,13 @@ type Settings struct { ProgrammaticRedirectDomainWhitelist []string `protobuf:"bytes,68,rep,name=programmatic_redirect_domain_whitelist,json=programmaticRedirectDomainWhitelist,proto3" json:"programmatic_redirect_domain_whitelist,omitempty"` AuditKey *crypt.PublicKeyEncryptionKey `protobuf:"bytes,72,opt,name=audit_key,json=auditKey,proto3,oneof" json:"audit_key,omitempty"` CodecType *v31.HttpConnectionManager_CodecType `protobuf:"varint,73,opt,name=codec_type,json=codecType,proto3,enum=envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager_CodecType,oneof" json:"codec_type,omitempty"` + PrimaryColor *string `protobuf:"bytes,85,opt,name=primary_color,json=primaryColor,proto3,oneof" json:"primary_color,omitempty"` + SecondaryColor *string `protobuf:"bytes,86,opt,name=secondary_color,json=secondaryColor,proto3,oneof" json:"secondary_color,omitempty"` + DarkmodePrimaryColor *string `protobuf:"bytes,87,opt,name=darkmode_primary_color,json=darkmodePrimaryColor,proto3,oneof" json:"darkmode_primary_color,omitempty"` + DarkmodeSecondaryColor *string `protobuf:"bytes,88,opt,name=darkmode_secondary_color,json=darkmodeSecondaryColor,proto3,oneof" json:"darkmode_secondary_color,omitempty"` + LogoUrl *string `protobuf:"bytes,89,opt,name=logo_url,json=logoUrl,proto3,oneof" json:"logo_url,omitempty"` + FaviconUrl *string `protobuf:"bytes,90,opt,name=favicon_url,json=faviconUrl,proto3,oneof" json:"favicon_url,omitempty"` + ErrorMessageFirstParagraph *string `protobuf:"bytes,91,opt,name=error_message_first_paragraph,json=errorMessageFirstParagraph,proto3,oneof" json:"error_message_first_paragraph,omitempty"` } func (x *Settings) Reset() { @@ -1557,6 +1564,55 @@ func (x *Settings) GetCodecType() v31.HttpConnectionManager_CodecType { return v31.HttpConnectionManager_CodecType(0) } +func (x *Settings) GetPrimaryColor() string { + if x != nil && x.PrimaryColor != nil { + return *x.PrimaryColor + } + return "" +} + +func (x *Settings) GetSecondaryColor() string { + if x != nil && x.SecondaryColor != nil { + return *x.SecondaryColor + } + return "" +} + +func (x *Settings) GetDarkmodePrimaryColor() string { + if x != nil && x.DarkmodePrimaryColor != nil { + return *x.DarkmodePrimaryColor + } + return "" +} + +func (x *Settings) GetDarkmodeSecondaryColor() string { + if x != nil && x.DarkmodeSecondaryColor != nil { + return *x.DarkmodeSecondaryColor + } + return "" +} + +func (x *Settings) GetLogoUrl() string { + if x != nil && x.LogoUrl != nil { + return *x.LogoUrl + } + return "" +} + +func (x *Settings) GetFaviconUrl() string { + if x != nil && x.FaviconUrl != nil { + return *x.FaviconUrl + } + return "" +} + +func (x *Settings) GetErrorMessageFirstParagraph() string { + if x != nil && x.ErrorMessageFirstParagraph != nil { + return *x.ErrorMessageFirstParagraph + } + return "" +} + type Settings_Certificate struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1926,7 +1982,7 @@ var file_config_proto_rawDesc = []byte{ 0x79, 0x12, 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xfa, 0x30, 0x0a, 0x08, 0x53, 0x65, 0x74, 0x74, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xf7, 0x34, 0x0a, 0x08, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x2c, 0x0a, 0x0f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x47, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x88, @@ -2200,7 +2256,29 @@ var file_config_proto_rawDesc = []byte{ 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x76, 0x33, 0x2e, 0x48, 0x74, 0x74, 0x70, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x43, 0x6f, 0x64, 0x65, 0x63, 0x54, 0x79, 0x70, 0x65, 0x48, 0x43, - 0x52, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x63, 0x54, 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x1a, 0x81, + 0x52, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x63, 0x54, 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x28, + 0x0a, 0x0d, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x18, + 0x55, 0x20, 0x01, 0x28, 0x09, 0x48, 0x44, 0x52, 0x0c, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, + 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x88, 0x01, 0x01, 0x12, 0x2c, 0x0a, 0x0f, 0x73, 0x65, 0x63, 0x6f, + 0x6e, 0x64, 0x61, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x18, 0x56, 0x20, 0x01, 0x28, + 0x09, 0x48, 0x45, 0x52, 0x0e, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x43, 0x6f, + 0x6c, 0x6f, 0x72, 0x88, 0x01, 0x01, 0x12, 0x39, 0x0a, 0x16, 0x64, 0x61, 0x72, 0x6b, 0x6d, 0x6f, + 0x64, 0x65, 0x5f, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x6c, 0x6f, 0x72, + 0x18, 0x57, 0x20, 0x01, 0x28, 0x09, 0x48, 0x46, 0x52, 0x14, 0x64, 0x61, 0x72, 0x6b, 0x6d, 0x6f, + 0x64, 0x65, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x88, 0x01, + 0x01, 0x12, 0x3d, 0x0a, 0x18, 0x64, 0x61, 0x72, 0x6b, 0x6d, 0x6f, 0x64, 0x65, 0x5f, 0x73, 0x65, + 0x63, 0x6f, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x18, 0x58, 0x20, + 0x01, 0x28, 0x09, 0x48, 0x47, 0x52, 0x16, 0x64, 0x61, 0x72, 0x6b, 0x6d, 0x6f, 0x64, 0x65, 0x53, + 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x88, 0x01, 0x01, + 0x12, 0x1e, 0x0a, 0x08, 0x6c, 0x6f, 0x67, 0x6f, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x59, 0x20, 0x01, + 0x28, 0x09, 0x48, 0x48, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x6f, 0x55, 0x72, 0x6c, 0x88, 0x01, 0x01, + 0x12, 0x24, 0x0a, 0x0b, 0x66, 0x61, 0x76, 0x69, 0x63, 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, + 0x5a, 0x20, 0x01, 0x28, 0x09, 0x48, 0x49, 0x52, 0x0a, 0x66, 0x61, 0x76, 0x69, 0x63, 0x6f, 0x6e, + 0x55, 0x72, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x46, 0x0a, 0x1d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, + 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x66, 0x69, 0x72, 0x73, 0x74, 0x5f, 0x70, 0x61, + 0x72, 0x61, 0x67, 0x72, 0x61, 0x70, 0x68, 0x18, 0x5b, 0x20, 0x01, 0x28, 0x09, 0x48, 0x4a, 0x52, + 0x1a, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x46, 0x69, 0x72, + 0x73, 0x74, 0x50, 0x61, 0x72, 0x61, 0x67, 0x72, 0x61, 0x70, 0x68, 0x88, 0x01, 0x01, 0x1a, 0x81, 0x01, 0x0a, 0x0b, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x65, 0x72, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6b, @@ -2318,10 +2396,20 @@ var file_config_proto_rawDesc = []byte{ 0x0a, 0x15, 0x5f, 0x78, 0x66, 0x66, 0x5f, 0x6e, 0x75, 0x6d, 0x5f, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x68, 0x6f, 0x70, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x61, 0x75, 0x64, 0x69, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x63, 0x5f, - 0x74, 0x79, 0x70, 0x65, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x70, 0x6f, 0x6d, 0x65, 0x72, 0x69, 0x75, 0x6d, 0x2f, 0x70, 0x6f, 0x6d, 0x65, - 0x72, 0x69, 0x75, 0x6d, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x79, 0x70, 0x65, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, + 0x5f, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, + 0x64, 0x61, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x42, 0x19, 0x0a, 0x17, 0x5f, 0x64, + 0x61, 0x72, 0x6b, 0x6d, 0x6f, 0x64, 0x65, 0x5f, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x5f, + 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x42, 0x1b, 0x0a, 0x19, 0x5f, 0x64, 0x61, 0x72, 0x6b, 0x6d, 0x6f, + 0x64, 0x65, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x6c, + 0x6f, 0x72, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x6c, 0x6f, 0x67, 0x6f, 0x5f, 0x75, 0x72, 0x6c, 0x42, + 0x0e, 0x0a, 0x0c, 0x5f, 0x66, 0x61, 0x76, 0x69, 0x63, 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x42, + 0x20, 0x0a, 0x1e, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x5f, 0x66, 0x69, 0x72, 0x73, 0x74, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x67, 0x72, 0x61, 0x70, + 0x68, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x70, 0x6f, 0x6d, 0x65, 0x72, 0x69, 0x75, 0x6d, 0x2f, 0x70, 0x6f, 0x6d, 0x65, 0x72, 0x69, 0x75, + 0x6d, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/pkg/grpc/config/config.proto b/pkg/grpc/config/config.proto index 9765d6203..d0ff58fbc 100644 --- a/pkg/grpc/config/config.proto +++ b/pkg/grpc/config/config.proto @@ -216,4 +216,11 @@ message Settings { optional pomerium.crypt.PublicKeyEncryptionKey audit_key = 72; optional envoy.extensions.filters.network.http_connection_manager.v3 .HttpConnectionManager.CodecType codec_type = 73; + optional string primary_color = 85; + optional string secondary_color = 86; + optional string darkmode_primary_color = 87; + optional string darkmode_secondary_color = 88; + optional string logo_url = 89; + optional string favicon_url = 90; + optional string error_message_first_paragraph = 91; }