v2
This commit is contained in:
544
internal/auth/saml.go
Normal file
544
internal/auth/saml.go
Normal file
@ -0,0 +1,544 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/kms/api-key-service/internal/config"
|
||||
"github.com/kms/api-key-service/internal/domain"
|
||||
"github.com/kms/api-key-service/internal/errors"
|
||||
)
|
||||
|
||||
// SAMLProvider represents a SAML 2.0 identity provider
|
||||
type SAMLProvider struct {
|
||||
config config.ConfigProvider
|
||||
logger *zap.Logger
|
||||
httpClient *http.Client
|
||||
privateKey *rsa.PrivateKey
|
||||
certificate *x509.Certificate
|
||||
}
|
||||
|
||||
// NewSAMLProvider creates a new SAML provider
|
||||
func NewSAMLProvider(config config.ConfigProvider, logger *zap.Logger) (*SAMLProvider, error) {
|
||||
provider := &SAMLProvider{
|
||||
config: config,
|
||||
logger: logger,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
// Load SP private key and certificate if configured
|
||||
if err := provider.loadCredentials(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
// SAMLMetadata represents SAML IdP metadata
|
||||
type SAMLMetadata struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata EntityDescriptor"`
|
||||
EntityID string `xml:"entityID,attr"`
|
||||
IDPSSODescriptor IDPSSODescriptor `xml:"urn:oasis:names:tc:SAML:2.0:metadata IDPSSODescriptor"`
|
||||
}
|
||||
|
||||
// IDPSSODescriptor represents the IdP SSO descriptor
|
||||
type IDPSSODescriptor struct {
|
||||
ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"`
|
||||
KeyDescriptor []KeyDescriptor `xml:"urn:oasis:names:tc:SAML:2.0:metadata KeyDescriptor"`
|
||||
SingleSignOnService []SingleSignOnService `xml:"urn:oasis:names:tc:SAML:2.0:metadata SingleSignOnService"`
|
||||
SingleLogoutService []SingleLogoutService `xml:"urn:oasis:names:tc:SAML:2.0:metadata SingleLogoutService"`
|
||||
}
|
||||
|
||||
// KeyDescriptor represents a key descriptor
|
||||
type KeyDescriptor struct {
|
||||
Use string `xml:"use,attr"`
|
||||
KeyInfo KeyInfo `xml:"urn:xmldsig KeyInfo"`
|
||||
}
|
||||
|
||||
// KeyInfo represents key information
|
||||
type KeyInfo struct {
|
||||
X509Data X509Data `xml:"urn:xmldsig X509Data"`
|
||||
}
|
||||
|
||||
// X509Data represents X509 certificate data
|
||||
type X509Data struct {
|
||||
X509Certificate string `xml:"urn:xmldsig X509Certificate"`
|
||||
}
|
||||
|
||||
// SingleSignOnService represents SSO service endpoint
|
||||
type SingleSignOnService struct {
|
||||
Binding string `xml:"Binding,attr"`
|
||||
Location string `xml:"Location,attr"`
|
||||
}
|
||||
|
||||
// SingleLogoutService represents SLO service endpoint
|
||||
type SingleLogoutService struct {
|
||||
Binding string `xml:"Binding,attr"`
|
||||
Location string `xml:"Location,attr"`
|
||||
}
|
||||
|
||||
// SAMLRequest represents a SAML authentication request
|
||||
type SAMLRequest struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol AuthnRequest"`
|
||||
ID string `xml:"ID,attr"`
|
||||
Version string `xml:"Version,attr"`
|
||||
IssueInstant time.Time `xml:"IssueInstant,attr"`
|
||||
Destination string `xml:"Destination,attr"`
|
||||
AssertionConsumerServiceURL string `xml:"AssertionConsumerServiceURL,attr"`
|
||||
ProtocolBinding string `xml:"ProtocolBinding,attr"`
|
||||
Issuer Issuer `xml:"urn:oasis:names:tc:SAML:2.0:assertion Issuer"`
|
||||
NameIDPolicy NameIDPolicy `xml:"urn:oasis:names:tc:SAML:2.0:protocol NameIDPolicy"`
|
||||
}
|
||||
|
||||
// Issuer represents the SAML issuer
|
||||
type Issuer struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Issuer"`
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// NameIDPolicy represents the name ID policy
|
||||
type NameIDPolicy struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol NameIDPolicy"`
|
||||
Format string `xml:"Format,attr"`
|
||||
}
|
||||
|
||||
// SAMLResponse represents a SAML response
|
||||
type SAMLResponse struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol Response"`
|
||||
ID string `xml:"ID,attr"`
|
||||
Version string `xml:"Version,attr"`
|
||||
IssueInstant time.Time `xml:"IssueInstant,attr"`
|
||||
Destination string `xml:"Destination,attr"`
|
||||
InResponseTo string `xml:"InResponseTo,attr"`
|
||||
Issuer Issuer `xml:"urn:oasis:names:tc:SAML:2.0:assertion Issuer"`
|
||||
Status Status `xml:"urn:oasis:names:tc:SAML:2.0:protocol Status"`
|
||||
Assertion Assertion `xml:"urn:oasis:names:tc:SAML:2.0:assertion Assertion"`
|
||||
}
|
||||
|
||||
// Status represents the SAML response status
|
||||
type Status struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol Status"`
|
||||
StatusCode StatusCode `xml:"urn:oasis:names:tc:SAML:2.0:protocol StatusCode"`
|
||||
}
|
||||
|
||||
// StatusCode represents the status code
|
||||
type StatusCode struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol StatusCode"`
|
||||
Value string `xml:"Value,attr"`
|
||||
}
|
||||
|
||||
// Assertion represents a SAML assertion
|
||||
type Assertion struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Assertion"`
|
||||
ID string `xml:"ID,attr"`
|
||||
Version string `xml:"Version,attr"`
|
||||
IssueInstant time.Time `xml:"IssueInstant,attr"`
|
||||
Issuer Issuer `xml:"urn:oasis:names:tc:SAML:2.0:assertion Issuer"`
|
||||
Subject Subject `xml:"urn:oasis:names:tc:SAML:2.0:assertion Subject"`
|
||||
Conditions Conditions `xml:"urn:oasis:names:tc:SAML:2.0:assertion Conditions"`
|
||||
AttributeStatement AttributeStatement `xml:"urn:oasis:names:tc:SAML:2.0:assertion AttributeStatement"`
|
||||
AuthnStatement AuthnStatement `xml:"urn:oasis:names:tc:SAML:2.0:assertion AuthnStatement"`
|
||||
}
|
||||
|
||||
// Subject represents the assertion subject
|
||||
type Subject struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Subject"`
|
||||
NameID NameID `xml:"urn:oasis:names:tc:SAML:2.0:assertion NameID"`
|
||||
SubjectConfirmation SubjectConfirmation `xml:"urn:oasis:names:tc:SAML:2.0:assertion SubjectConfirmation"`
|
||||
}
|
||||
|
||||
// NameID represents the name identifier
|
||||
type NameID struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion NameID"`
|
||||
Format string `xml:"Format,attr"`
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// SubjectConfirmation represents subject confirmation
|
||||
type SubjectConfirmation struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion SubjectConfirmation"`
|
||||
Method string `xml:"Method,attr"`
|
||||
SubjectConfirmationData SubjectConfirmationData `xml:"urn:oasis:names:tc:SAML:2.0:assertion SubjectConfirmationData"`
|
||||
}
|
||||
|
||||
// SubjectConfirmationData represents subject confirmation data
|
||||
type SubjectConfirmationData struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion SubjectConfirmationData"`
|
||||
InResponseTo string `xml:"InResponseTo,attr"`
|
||||
NotOnOrAfter time.Time `xml:"NotOnOrAfter,attr"`
|
||||
Recipient string `xml:"Recipient,attr"`
|
||||
}
|
||||
|
||||
// Conditions represents assertion conditions
|
||||
type Conditions struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Conditions"`
|
||||
NotBefore time.Time `xml:"NotBefore,attr"`
|
||||
NotOnOrAfter time.Time `xml:"NotOnOrAfter,attr"`
|
||||
AudienceRestriction AudienceRestriction `xml:"urn:oasis:names:tc:SAML:2.0:assertion AudienceRestriction"`
|
||||
}
|
||||
|
||||
// AudienceRestriction represents audience restriction
|
||||
type AudienceRestriction struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion AudienceRestriction"`
|
||||
Audience Audience `xml:"urn:oasis:names:tc:SAML:2.0:assertion Audience"`
|
||||
}
|
||||
|
||||
// Audience represents the intended audience
|
||||
type Audience struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Audience"`
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// AttributeStatement represents attribute statement
|
||||
type AttributeStatement struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion AttributeStatement"`
|
||||
Attribute []Attribute `xml:"urn:oasis:names:tc:SAML:2.0:assertion Attribute"`
|
||||
}
|
||||
|
||||
// Attribute represents a SAML attribute
|
||||
type Attribute struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Attribute"`
|
||||
Name string `xml:"Name,attr"`
|
||||
AttributeValue []AttributeValue `xml:"urn:oasis:names:tc:SAML:2.0:assertion AttributeValue"`
|
||||
}
|
||||
|
||||
// AttributeValue represents an attribute value
|
||||
type AttributeValue struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion AttributeValue"`
|
||||
Type string `xml:"http://www.w3.org/2001/XMLSchema-instance type,attr"`
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// AuthnStatement represents authentication statement
|
||||
type AuthnStatement struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion AuthnStatement"`
|
||||
AuthnInstant time.Time `xml:"AuthnInstant,attr"`
|
||||
SessionIndex string `xml:"SessionIndex,attr"`
|
||||
AuthnContext AuthnContext `xml:"urn:oasis:names:tc:SAML:2.0:assertion AuthnContext"`
|
||||
}
|
||||
|
||||
// AuthnContext represents authentication context
|
||||
type AuthnContext struct {
|
||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion AuthnContext"`
|
||||
AuthnContextClassRef string `xml:"urn:oasis:names:tc:SAML:2.0:assertion AuthnContextClassRef"`
|
||||
}
|
||||
|
||||
// GetMetadata fetches the SAML IdP metadata
|
||||
func (p *SAMLProvider) GetMetadata(ctx context.Context) (*SAMLMetadata, error) {
|
||||
metadataURL := p.config.GetString("SAML_IDP_METADATA_URL")
|
||||
if metadataURL == "" {
|
||||
return nil, errors.NewConfigurationError("SAML_IDP_METADATA_URL not configured")
|
||||
}
|
||||
|
||||
p.logger.Debug("Fetching SAML IdP metadata", zap.String("url", metadataURL))
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", metadataURL, nil)
|
||||
if err != nil {
|
||||
return nil, errors.NewInternalError("Failed to create metadata request").WithInternal(err)
|
||||
}
|
||||
|
||||
resp, err := p.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.NewInternalError("Failed to fetch IdP metadata").WithInternal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.NewInternalError(fmt.Sprintf("Metadata endpoint returned status %d", resp.StatusCode))
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, errors.NewInternalError("Failed to read metadata response").WithInternal(err)
|
||||
}
|
||||
|
||||
var metadata SAMLMetadata
|
||||
if err := xml.Unmarshal(body, &metadata); err != nil {
|
||||
return nil, errors.NewInternalError("Failed to parse SAML metadata").WithInternal(err)
|
||||
}
|
||||
|
||||
p.logger.Debug("SAML IdP metadata fetched successfully",
|
||||
zap.String("entity_id", metadata.EntityID))
|
||||
|
||||
return &metadata, nil
|
||||
}
|
||||
|
||||
// GenerateAuthRequest generates a SAML authentication request
|
||||
func (p *SAMLProvider) GenerateAuthRequest(ctx context.Context, relayState string) (string, string, error) {
|
||||
metadata, err := p.GetMetadata(ctx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Find SSO endpoint
|
||||
var ssoEndpoint string
|
||||
for _, sso := range metadata.IDPSSODescriptor.SingleSignOnService {
|
||||
if sso.Binding == "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" {
|
||||
ssoEndpoint = sso.Location
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ssoEndpoint == "" {
|
||||
return "", "", errors.NewConfigurationError("No HTTP-Redirect SSO endpoint found in IdP metadata")
|
||||
}
|
||||
|
||||
// Generate request ID
|
||||
requestID := "_" + uuid.New().String()
|
||||
|
||||
// Get SP configuration
|
||||
spEntityID := p.config.GetString("SAML_SP_ENTITY_ID")
|
||||
acsURL := p.config.GetString("SAML_SP_ACS_URL")
|
||||
|
||||
if spEntityID == "" {
|
||||
return "", "", errors.NewConfigurationError("SAML_SP_ENTITY_ID not configured")
|
||||
}
|
||||
if acsURL == "" {
|
||||
return "", "", errors.NewConfigurationError("SAML_SP_ACS_URL not configured")
|
||||
}
|
||||
|
||||
// Create SAML request
|
||||
samlRequest := SAMLRequest{
|
||||
ID: requestID,
|
||||
Version: "2.0",
|
||||
IssueInstant: time.Now().UTC(),
|
||||
Destination: ssoEndpoint,
|
||||
AssertionConsumerServiceURL: acsURL,
|
||||
ProtocolBinding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
||||
Issuer: Issuer{
|
||||
Value: spEntityID,
|
||||
},
|
||||
NameIDPolicy: NameIDPolicy{
|
||||
Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress",
|
||||
},
|
||||
}
|
||||
|
||||
// Marshal to XML
|
||||
xmlData, err := xml.MarshalIndent(samlRequest, "", " ")
|
||||
if err != nil {
|
||||
return "", "", errors.NewInternalError("Failed to marshal SAML request").WithInternal(err)
|
||||
}
|
||||
|
||||
// Add XML declaration
|
||||
xmlRequest := `<?xml version="1.0" encoding="UTF-8"?>` + "\n" + string(xmlData)
|
||||
|
||||
// Base64 encode and URL encode
|
||||
encodedRequest := base64.StdEncoding.EncodeToString([]byte(xmlRequest))
|
||||
|
||||
// Build redirect URL
|
||||
params := url.Values{
|
||||
"SAMLRequest": {encodedRequest},
|
||||
"RelayState": {relayState},
|
||||
}
|
||||
|
||||
redirectURL := ssoEndpoint + "?" + params.Encode()
|
||||
|
||||
p.logger.Debug("Generated SAML authentication request",
|
||||
zap.String("request_id", requestID),
|
||||
zap.String("sso_endpoint", ssoEndpoint))
|
||||
|
||||
return redirectURL, requestID, nil
|
||||
}
|
||||
|
||||
// ProcessSAMLResponse processes a SAML response and extracts user information
|
||||
func (p *SAMLProvider) ProcessSAMLResponse(ctx context.Context, samlResponse string, expectedRequestID string) (*domain.AuthContext, error) {
|
||||
p.logger.Debug("Processing SAML response")
|
||||
|
||||
// Base64 decode the response
|
||||
decodedResponse, err := base64.StdEncoding.DecodeString(samlResponse)
|
||||
if err != nil {
|
||||
return nil, errors.NewValidationError("Failed to decode SAML response").WithInternal(err)
|
||||
}
|
||||
|
||||
// Parse XML
|
||||
var response SAMLResponse
|
||||
if err := xml.Unmarshal(decodedResponse, &response); err != nil {
|
||||
return nil, errors.NewValidationError("Failed to parse SAML response").WithInternal(err)
|
||||
}
|
||||
|
||||
// Validate response
|
||||
if err := p.validateSAMLResponse(&response, expectedRequestID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Extract user information from assertion
|
||||
authContext, err := p.extractUserInfo(&response.Assertion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.logger.Debug("SAML response processed successfully",
|
||||
zap.String("user_id", authContext.UserID))
|
||||
|
||||
return authContext, nil
|
||||
}
|
||||
|
||||
// validateSAMLResponse validates a SAML response
|
||||
func (p *SAMLProvider) validateSAMLResponse(response *SAMLResponse, expectedRequestID string) error {
|
||||
// Check status
|
||||
if response.Status.StatusCode.Value != "urn:oasis:names:tc:SAML:2.0:status:Success" {
|
||||
return errors.NewAuthenticationError("SAML authentication failed: " + response.Status.StatusCode.Value)
|
||||
}
|
||||
|
||||
// Validate InResponseTo
|
||||
if expectedRequestID != "" && response.InResponseTo != expectedRequestID {
|
||||
return errors.NewValidationError("SAML response InResponseTo does not match request ID")
|
||||
}
|
||||
|
||||
// Validate assertion conditions
|
||||
assertion := &response.Assertion
|
||||
now := time.Now().UTC()
|
||||
|
||||
if now.Before(assertion.Conditions.NotBefore) {
|
||||
return errors.NewValidationError("SAML assertion not yet valid")
|
||||
}
|
||||
|
||||
if now.After(assertion.Conditions.NotOnOrAfter) {
|
||||
return errors.NewValidationError("SAML assertion has expired")
|
||||
}
|
||||
|
||||
// Validate audience
|
||||
expectedAudience := p.config.GetString("SAML_SP_ENTITY_ID")
|
||||
if assertion.Conditions.AudienceRestriction.Audience.Value != expectedAudience {
|
||||
return errors.NewValidationError("SAML assertion audience mismatch")
|
||||
}
|
||||
|
||||
// In production, you should also validate the signature
|
||||
// This requires implementing XML signature validation
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractUserInfo extracts user information from SAML assertion
|
||||
func (p *SAMLProvider) extractUserInfo(assertion *Assertion) (*domain.AuthContext, error) {
|
||||
// Extract user ID from NameID
|
||||
userID := assertion.Subject.NameID.Value
|
||||
if userID == "" {
|
||||
return nil, errors.NewValidationError("SAML assertion missing NameID")
|
||||
}
|
||||
|
||||
// Extract attributes
|
||||
claims := make(map[string]string)
|
||||
claims["sub"] = userID
|
||||
claims["name_id_format"] = assertion.Subject.NameID.Format
|
||||
|
||||
// Process attribute statements
|
||||
for _, attr := range assertion.AttributeStatement.Attribute {
|
||||
if len(attr.AttributeValue) > 0 {
|
||||
// Use the first value if multiple values exist
|
||||
claims[attr.Name] = attr.AttributeValue[0].Value
|
||||
}
|
||||
}
|
||||
|
||||
// Map common attributes to standard claims
|
||||
if email, exists := claims["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"]; exists {
|
||||
claims["email"] = email
|
||||
}
|
||||
if name, exists := claims["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"]; exists {
|
||||
claims["name"] = name
|
||||
}
|
||||
if givenName, exists := claims["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"]; exists {
|
||||
claims["given_name"] = givenName
|
||||
}
|
||||
if surname, exists := claims["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"]; exists {
|
||||
claims["family_name"] = surname
|
||||
}
|
||||
|
||||
// Extract permissions/roles if available
|
||||
var permissions []string
|
||||
if roles, exists := claims["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"]; exists {
|
||||
permissions = strings.Split(roles, ",")
|
||||
}
|
||||
|
||||
authContext := &domain.AuthContext{
|
||||
UserID: userID,
|
||||
TokenType: domain.TokenTypeUser,
|
||||
Claims: claims,
|
||||
Permissions: permissions,
|
||||
}
|
||||
|
||||
return authContext, nil
|
||||
}
|
||||
|
||||
// GenerateServiceProviderMetadata generates SP metadata XML
|
||||
func (p *SAMLProvider) GenerateServiceProviderMetadata() (string, error) {
|
||||
spEntityID := p.config.GetString("SAML_SP_ENTITY_ID")
|
||||
acsURL := p.config.GetString("SAML_SP_ACS_URL")
|
||||
|
||||
if spEntityID == "" {
|
||||
return "", errors.NewConfigurationError("SAML_SP_ENTITY_ID not configured")
|
||||
}
|
||||
if acsURL == "" {
|
||||
return "", errors.NewConfigurationError("SAML_SP_ACS_URL not configured")
|
||||
}
|
||||
|
||||
// This is a simplified SP metadata generation
|
||||
// In production, you should use a proper SAML library
|
||||
metadata := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="%s">
|
||||
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="%s" index="0"/>
|
||||
</md:SPSSODescriptor>
|
||||
</md:EntityDescriptor>`, spEntityID, acsURL)
|
||||
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
// loadCredentials loads SP private key and certificate
|
||||
func (p *SAMLProvider) loadCredentials() error {
|
||||
// Load private key if configured
|
||||
privateKeyPEM := p.config.GetString("SAML_SP_PRIVATE_KEY")
|
||||
if privateKeyPEM != "" {
|
||||
block, _ := pem.Decode([]byte(privateKeyPEM))
|
||||
if block == nil {
|
||||
return errors.NewConfigurationError("Failed to decode SAML SP private key")
|
||||
}
|
||||
|
||||
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
// Try PKCS8 format
|
||||
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return errors.NewConfigurationError("Failed to parse SAML SP private key").WithInternal(err)
|
||||
}
|
||||
var ok bool
|
||||
privateKey, ok = key.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return errors.NewConfigurationError("SAML SP private key is not RSA")
|
||||
}
|
||||
}
|
||||
p.privateKey = privateKey
|
||||
}
|
||||
|
||||
// Load certificate if configured
|
||||
certificatePEM := p.config.GetString("SAML_SP_CERTIFICATE")
|
||||
if certificatePEM != "" {
|
||||
block, _ := pem.Decode([]byte(certificatePEM))
|
||||
if block == nil {
|
||||
return errors.NewConfigurationError("Failed to decode SAML SP certificate")
|
||||
}
|
||||
|
||||
certificate, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return errors.NewConfigurationError("Failed to parse SAML SP certificate").WithInternal(err)
|
||||
}
|
||||
p.certificate = certificate
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user