Commit 31dfb54b authored by Eric Chiang's avatar Eric Chiang

connector: add a SAML connector

parent ec9d1607
...@@ -66,6 +66,23 @@ type CallbackConnector interface { ...@@ -66,6 +66,23 @@ type CallbackConnector interface {
HandleCallback(s Scopes, r *http.Request) (identity Identity, err error) HandleCallback(s Scopes, r *http.Request) (identity Identity, err error)
} }
// SAMLConnector represents SAML connectors which implement the HTTP POST binding.
//
// RelayState is handled by the server.
type SAMLConnector interface {
// POSTData returns an encoded SAML request and SSO URL for the server to
// render a POST form with.
POSTData(s Scopes) (sooURL, samlRequest string, err error)
// TODO(ericchiang): Provide expected "InResponseTo" ID value.
//
// See: https://www.oasis-open.org/committees/download.php/35711/sstc-saml-core-errata-2.0-wd-06-diff.pdf
// "3.2.2 Complex Type StatusResponseType"
// HandlePOST decodes, verifies, and maps attributes from the SAML response.
HandlePOST(s Scopes, samlResponse string) (identity Identity, err error)
}
// RefreshConnector is a connector that can update the client claims. // RefreshConnector is a connector that can update the client claims.
type RefreshConnector interface { type RefreshConnector interface {
// Refresh is called when a client attempts to claim a refresh token. The // Refresh is called when a client attempts to claim a refresh token. The
......
This diff is collapsed.
package saml
import (
"crypto/x509"
"encoding/pem"
"errors"
"io/ioutil"
"testing"
sdig "github.com/russellhaering/goxmldsig"
)
func loadCert(ca string) (*x509.Certificate, error) {
data, err := ioutil.ReadFile(ca)
if err != nil {
return nil, err
}
block, _ := pem.Decode(data)
if block == nil {
return nil, errors.New("ca file didn't contain any PEM data")
}
return x509.ParseCertificate(block.Bytes)
}
func TestVerify(t *testing.T) {
cert, err := loadCert("testdata/okta-ca.pem")
if err != nil {
t.Fatal(err)
}
s := certStore{[]*x509.Certificate{cert}}
validator := sdig.NewDefaultValidationContext(s)
data, err := ioutil.ReadFile("testdata/okta-resp.xml")
if err != nil {
t.Fatal(err)
}
if _, err := verify(validator, data); err != nil {
t.Fatal(err)
}
}
-----BEGIN CERTIFICATE-----
MIIDpDCCAoygAwIBAgIGAVjgvNroMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG
A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU
MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi05NjkyNDQxHDAaBgkqhkiG9w0BCQEW
DWluZm9Ab2t0YS5jb20wHhcNMTYxMjA4MjMxOTIzWhcNMjYxMjA4MjMyMDIzWjCBkjELMAkGA1UE
BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV
BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtOTY5MjQ0MRwwGgYJ
KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
4dW2YlcXjZwnTmLnV7IOBq8hhrdbqlNwdjCHRyx1BizOk3RbVP56grgPdyWScTCPpJ6vZZ8rtrY0
m1rwr+cxifNuQGKTlE33g2hReo/N9f3LFUMITlnnNH80Yium3SYuEqGeHLYerelXOnEKx6x+X5qD
eg2DRW6I9/v/mfN2KAQEDqF9aSNlNFWZWmb52kukMv3tLWw0puaevicIZ/nZrW+D3CLDVVfWHeVt
46EF2bkLdgbIJOU3GzLoolgBOCkydX9x6xTw6knwQaqYsRGflacw6571IzWEwjmd17uJXkarnhM1
51pqwIoksTzycbjinIg6B1rNpGFDN7Ah+9EnVQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQDQOtlj
7Yk1j4GV/135zLOlsaonq8zfsu05VfY5XydNPFEkyIVcegLa8RiqALyFf+FjY/wVyhXtARw7NBUo
u/Jh63cVya/VEoP1SVa0XQLf/ial+XuwdBBL1yc+7o2XHOfluDaXw/v2FWYpZtICf3a39giGaNWp
eCT4g2TDWM4Jf/X7/mRbLX9tQO7XRural1CXx8VIMcmbKNbUtQiO8yEtVQ+FJKOsl7KOSzkgqNiL
rJy+Y0D9biLZVKp07KWAY2FPCEtCkBrvo8BhvWbxWMA8CVQNAiTylM27Pc6kbc64pNr7C1Jx1wuE
mVy9Fgb4PA2N3hPeD7mBmGGp7CfDbGcy
-----END CERTIFICATE-----
<?xml version="1.0" encoding="UTF-8"?><saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" Destination="http://localhost:5556/dex/callback" ID="id108965453120986171998428970" InResponseTo="_fd1b3ef9-ec09-44a7-a66b-0d39c250f6a0" IssueInstant="2016-12-20T22:18:23.771Z" Version="2.0"><saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://www.okta.com/exk91cb99lKkKSYoy0h7</saml2:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI="#id108965453120986171998428970"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>Phu93l0D97JSMIYDZBdVeNLN0pwBVHhzUDWxbh4sc6g=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>M2gMHOmnMAFgh2apq/2jHwDYmisUkYMUqxrWkQJf3RHFotl4EeDlcqq/FzOboJc3NcbKBqQY3CWsWhWh5cNWHDgNneaahW4czww+9DCM0R/zz5c6GuMYFEh5df2sDn/dWk/jbKMiAMgPdKJ2x/+5Xk9q4axC52TdQrrbZtzAAAn4CgrT6Kf11qfMl5wpDarg3qPw7ANxWn2DKzCsvCkOIwM2+AXh+sEXmTvvZIQ0vpv098FH/ZTGt4sCwb1bmRZ3UZLhBcxVc/sjuEW/sQ6pbQHkjrXIR5bxXzGNUxYpcGjrp9HGF+In0BAc+Ds/A0H142e1rgtcX8LH2pbG8URJSQ==</ds:SignatureValue><ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIDpDCCAoygAwIBAgIGAVjgvNroMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG
A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU
MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi05NjkyNDQxHDAaBgkqhkiG9w0BCQEW
DWluZm9Ab2t0YS5jb20wHhcNMTYxMjA4MjMxOTIzWhcNMjYxMjA4MjMyMDIzWjCBkjELMAkGA1UE
BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV
BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtOTY5MjQ0MRwwGgYJ
KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
4dW2YlcXjZwnTmLnV7IOBq8hhrdbqlNwdjCHRyx1BizOk3RbVP56grgPdyWScTCPpJ6vZZ8rtrY0
m1rwr+cxifNuQGKTlE33g2hReo/N9f3LFUMITlnnNH80Yium3SYuEqGeHLYerelXOnEKx6x+X5qD
eg2DRW6I9/v/mfN2KAQEDqF9aSNlNFWZWmb52kukMv3tLWw0puaevicIZ/nZrW+D3CLDVVfWHeVt
46EF2bkLdgbIJOU3GzLoolgBOCkydX9x6xTw6knwQaqYsRGflacw6571IzWEwjmd17uJXkarnhM1
51pqwIoksTzycbjinIg6B1rNpGFDN7Ah+9EnVQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQDQOtlj
7Yk1j4GV/135zLOlsaonq8zfsu05VfY5XydNPFEkyIVcegLa8RiqALyFf+FjY/wVyhXtARw7NBUo
u/Jh63cVya/VEoP1SVa0XQLf/ial+XuwdBBL1yc+7o2XHOfluDaXw/v2FWYpZtICf3a39giGaNWp
eCT4g2TDWM4Jf/X7/mRbLX9tQO7XRural1CXx8VIMcmbKNbUtQiO8yEtVQ+FJKOsl7KOSzkgqNiL
rJy+Y0D9biLZVKp07KWAY2FPCEtCkBrvo8BhvWbxWMA8CVQNAiTylM27Pc6kbc64pNr7C1Jx1wuE
mVy9Fgb4PA2N3hPeD7mBmGGp7CfDbGcy</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature><saml2p:Status xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol"><saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></saml2p:Status><saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" ID="id10896545312129779529177535" IssueInstant="2016-12-20T22:18:23.771Z" Version="2.0"><saml2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">http://www.okta.com/exk91cb99lKkKSYoy0h7</saml2:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI="#id10896545312129779529177535"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>ufwWUjecX6I/aQb4WW9P9ZMLG3C8hN6LaZyyb/EATIs=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>jKtNBzxAL67ssuzWkkbf0yzqRyZ51y2JjBQ9C6bW8io/JOYQB2v7Bix7Eu/RjJslO7OBqD+3tPrK7ZBOy2+LFuAh3cDNa3U5NhO0raLrn/2YoJXfjj3XX3hyQv6GVxo0EY1KJNXOzWxjp9RVDpHslPTIL1yDC/oy0Mlzxu6pXBEerz9J2/Caenq66Skb5/DAT8FvrJ2s1bxuMagShs3APhC1hD8mvktZ+ZcN8ujs2SebteGK4IoOCx+e8+v2CyycBv1l5l+v5I+D2HnbAw4LfvHnW4rZOJT2AvoI47p1YBK1qDsJutG3jUPKy4Yx5YF73Xi1oytr+rrHyx/lfFPd2A==</ds:SignatureValue><ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIDpDCCAoygAwIBAgIGAVjgvNroMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG
A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU
MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi05NjkyNDQxHDAaBgkqhkiG9w0BCQEW
DWluZm9Ab2t0YS5jb20wHhcNMTYxMjA4MjMxOTIzWhcNMjYxMjA4MjMyMDIzWjCBkjELMAkGA1UE
BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV
BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtOTY5MjQ0MRwwGgYJ
KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
4dW2YlcXjZwnTmLnV7IOBq8hhrdbqlNwdjCHRyx1BizOk3RbVP56grgPdyWScTCPpJ6vZZ8rtrY0
m1rwr+cxifNuQGKTlE33g2hReo/N9f3LFUMITlnnNH80Yium3SYuEqGeHLYerelXOnEKx6x+X5qD
eg2DRW6I9/v/mfN2KAQEDqF9aSNlNFWZWmb52kukMv3tLWw0puaevicIZ/nZrW+D3CLDVVfWHeVt
46EF2bkLdgbIJOU3GzLoolgBOCkydX9x6xTw6knwQaqYsRGflacw6571IzWEwjmd17uJXkarnhM1
51pqwIoksTzycbjinIg6B1rNpGFDN7Ah+9EnVQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQDQOtlj
7Yk1j4GV/135zLOlsaonq8zfsu05VfY5XydNPFEkyIVcegLa8RiqALyFf+FjY/wVyhXtARw7NBUo
u/Jh63cVya/VEoP1SVa0XQLf/ial+XuwdBBL1yc+7o2XHOfluDaXw/v2FWYpZtICf3a39giGaNWp
eCT4g2TDWM4Jf/X7/mRbLX9tQO7XRural1CXx8VIMcmbKNbUtQiO8yEtVQ+FJKOsl7KOSzkgqNiL
rJy+Y0D9biLZVKp07KWAY2FPCEtCkBrvo8BhvWbxWMA8CVQNAiTylM27Pc6kbc64pNr7C1Jx1wuE
mVy9Fgb4PA2N3hPeD7mBmGGp7CfDbGcy</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature><saml2:Subject xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"><saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">eric.chiang+okta@coreos.com</saml2:NameID><saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml2:SubjectConfirmationData InResponseTo="_fd1b3ef9-ec09-44a7-a66b-0d39c250f6a0" NotOnOrAfter="2016-12-20T22:23:23.772Z" Recipient="http://localhost:5556/dex/callback"/></saml2:SubjectConfirmation></saml2:Subject><saml2:Conditions NotBefore="2016-12-20T22:13:23.772Z" NotOnOrAfter="2016-12-20T22:23:23.772Z" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"><saml2:AudienceRestriction><saml2:Audience>http://localhost:5556/dex/callback</saml2:Audience></saml2:AudienceRestriction></saml2:Conditions><saml2:AuthnStatement AuthnInstant="2016-12-20T22:18:23.771Z" SessionIndex="_fd1b3ef9-ec09-44a7-a66b-0d39c250f6a0" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"><saml2:AuthnContext><saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml2:AuthnContextClassRef></saml2:AuthnContext></saml2:AuthnStatement></saml2:Assertion></saml2p:Response>
package saml
import (
"encoding/xml"
"fmt"
"time"
)
const timeFormat = "2006-01-02T15:04:05Z"
type xmlTime time.Time
func (t xmlTime) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
return xml.Attr{
Name: name,
Value: time.Time(t).UTC().Format(timeFormat),
}, nil
}
func (t *xmlTime) UnmarshalXMLAttr(attr xml.Attr) error {
got, err := time.Parse(timeFormat, attr.Value)
if err != nil {
return err
}
*t = xmlTime(got)
return nil
}
type samlVersion struct{}
func (s samlVersion) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
return xml.Attr{
Name: name,
Value: "2.0",
}, nil
}
func (s *samlVersion) UnmarshalXMLAttr(attr xml.Attr) error {
if attr.Value != "2.0" {
return fmt.Errorf(`saml version expected "2.0" got %q`, attr.Value)
}
return nil
}
type authnRequest struct {
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol AuthnRequest"`
ID string `xml:"ID,attr"`
Version samlVersion `xml:"Version,attr"`
ProviderName string `xml:"ProviderName,attr,omitempty"`
IssueInstant xmlTime `xml:"IssueInstant,attr,omitempty"`
Consent bool `xml:"Consent,attr,omitempty"`
Destination string `xml:"Destination,attr,omitempty"`
ForceAuthn bool `xml:"ForceAuthn,attr,omitempty"`
IsPassive bool `xml:"IsPassive,attr,omitempty"`
ProtocolBinding string `xml:"ProtocolBinding,attr,omitempty"`
Subject *subject `xml:"Subject,omitempty"`
Issuer *issuer `xml:"Issuer,omitempty"`
NameIDPolicy *nameIDPolicy `xml:"NameIDPolicy,omitempty"`
// TODO(ericchiang): Make this configurable and determine appropriate default values.
RequestAuthnContext *requestAuthnContext `xml:"RequestAuthnContext,omitempty"`
}
type subject struct {
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Subject"`
NameID *nameID `xml:"NameID,omitempty"`
// TODO(ericchiang): Do we need to deal with baseID?
}
type nameID struct {
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion NameID"`
Format string `xml:"Format,omitempty"`
Value string `xml:",chardata"`
}
type issuer struct {
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Issuer"`
Issuer string `xml:",chardata"`
}
type nameIDPolicy struct {
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol NameIDPolicy"`
AllowCreate bool `xml:"AllowCreate,attr,omitempty"`
Format string `xml:"Format,attr,omitempty"`
}
type requestAuthnContext struct {
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol RequestAuthnContext"`
AuthnContextClassRefs []authnContextClassRef
}
type authnContextClassRef struct {
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol AuthnContextClassRef"`
Value string `xml:",chardata"`
}
type response struct {
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol Response"`
ID string `xml:"ID,attr"`
Version samlVersion `xml:"Version,attr"`
Destination string `xml:"Destination,attr,omitempty"`
Issuer *issuer `xml:"Issuer,omitempty"`
// TODO(ericchiang): How do deal with multiple assertions?
Assertion *assertion `xml:"Assertion,omitempty"`
}
type assertion struct {
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Assertion"`
Version samlVersion `xml:"Version,attr"`
ID string `xml:"ID,attr"`
IssueInstance xmlTime `xml:"IssueInstance,attr"`
Issuer issuer `xml:"Issuer"`
Subject *subject `xml:"Subject,omitempty"`
AttributeStatement *attributeStatement `xml:"AttributeStatement,omitempty"`
}
type attributeStatement struct {
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion AttributeStatement"`
Attributes []attribute `xml:"Attribute"`
}
func (a *attributeStatement) get(name string) (s string, ok bool) {
for _, attr := range a.Attributes {
if attr.Name == name {
ok = true
if len(attr.AttributeValues) > 0 {
return attr.AttributeValues[0].Value, true
}
}
}
return
}
func (a *attributeStatement) all(name string) (s []string, ok bool) {
for _, attr := range a.Attributes {
if attr.Name == name {
ok = true
for _, val := range attr.AttributeValues {
s = append(s, val.Value)
}
}
}
return
}
type attribute struct {
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Attribute"`
Name string `xml:"Name,attr"`
NameFormat string `xml:"NameFormat,attr,omitempty"`
FriendlyName string `xml:"FriendlyName,attr,omitempty"`
AttributeValues []attributeValue `xml:"AttributeValue,omitempty"`
}
type attributeValue struct {
XMLName xml.Name `xml:"AttributeValue"`
Value string `xml:",chardata"`
}
...@@ -131,3 +131,11 @@ import: ...@@ -131,3 +131,11 @@ import:
version: v0.11.0 version: v0.11.0
- package: golang.org/x/sys/unix - package: golang.org/x/sys/unix
version: 833a04a10549a95dc34458c195cbad61bbb6cb4d version: 833a04a10549a95dc34458c195cbad61bbb6cb4d
# XML signature validation for SAML connector
- package: github.com/russellhaering/goxmldsig
version: d9f653eb27ee8b145f7d5a45172e81a93def0860
- package: github.com/beevik/etree
version: 4cd0dd976db869f817248477718071a28e978df0
- package: github.com/jonboulle/clockwork
version: bcac9884e7502bb2b474c0339d889cb981a2f27f
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment