Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in
Toggle navigation
D
dex
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Commits
Issue Boards
Open sidebar
go
dex
Commits
31dfb54b
Commit
31dfb54b
authored
Dec 21, 2016
by
Eric Chiang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
connector: add a SAML connector
parent
ec9d1607
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
683 additions
and
0 deletions
+683
-0
connector.go
connector/connector.go
+17
-0
saml.go
connector/saml/saml.go
+387
-0
saml_test.go
connector/saml/saml_test.go
+42
-0
okta-ca.pem
connector/saml/testdata/okta-ca.pem
+19
-0
okta-resp.xml
connector/saml/testdata/okta-resp.xml
+33
-0
types.go
connector/saml/types.go
+177
-0
glide.yaml
glide.yaml
+8
-0
No files found.
connector/connector.go
View file @
31dfb54b
...
...
@@ -66,6 +66,23 @@ type CallbackConnector interface {
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.
type
RefreshConnector
interface
{
// Refresh is called when a client attempts to claim a refresh token. The
...
...
connector/saml/saml.go
0 → 100644
View file @
31dfb54b
// Package saml contains login methods for SAML.
package
saml
import
(
"bytes"
"compress/flate"
"crypto/rand"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"encoding/xml"
"errors"
"fmt"
"io/ioutil"
"strings"
"time"
"github.com/Sirupsen/logrus"
"github.com/beevik/etree"
dsig
"github.com/russellhaering/goxmldsig"
"github.com/coreos/dex/connector"
)
const
(
bindingRedirect
=
"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
bindingPOST
=
"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
nameIDFormatEmailAddress
=
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
nameIDFormatUnspecified
=
"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
nameIDFormatX509Subject
=
"urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName"
nameIDFormatWindowsDN
=
"urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName"
nameIDFormatEncrypted
=
"urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted"
nameIDFormatEntity
=
"urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
nameIDFormatKerberos
=
"urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos"
nameIDFormatPersistent
=
"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
nameIDformatTransient
=
"urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
)
var
(
nameIDFormats
=
[]
string
{
nameIDFormatEmailAddress
,
nameIDFormatUnspecified
,
nameIDFormatX509Subject
,
nameIDFormatWindowsDN
,
nameIDFormatEncrypted
,
nameIDFormatEntity
,
nameIDFormatKerberos
,
nameIDFormatPersistent
,
nameIDformatTransient
,
}
nameIDFormatLookup
=
make
(
map
[
string
]
string
)
)
func
init
()
{
suffix
:=
func
(
s
,
sep
string
)
string
{
if
i
:=
strings
.
LastIndex
(
s
,
sep
);
i
>
0
{
return
s
[
i
+
1
:
]
}
return
s
}
for
_
,
format
:=
range
nameIDFormats
{
nameIDFormatLookup
[
suffix
(
format
,
":"
)]
=
format
nameIDFormatLookup
[
format
]
=
format
}
}
// Config represents configuration options for the SAML provider.
type
Config
struct
{
// TODO(ericchiang): A bunch of these fields could be auto-filled if
// we supported SAML metadata discovery.
//
// https://www.oasis-open.org/committees/download.php/35391/sstc-saml-metadata-errata-2.0-wd-04-diff.pdf
Issuer
string
`json:"issuer"`
SSOURL
string
`json:"ssoURL"`
// X509 CA file or raw data to verify XML signatures.
CA
string
`json:"ca"`
CAData
[]
byte
`json:"caData"`
InsecureSkipSignatureValidation
bool
`json:"insecureSkipSignatureValidation"`
// Assertion attribute names to lookup various claims with.
UsernameAttr
string
`json:"usernameAttr"`
EmailAttr
string
`json:"emailAttr"`
GroupsAttr
string
`json:"groupsAttr"`
// If GroupsDelim is supplied the connector assumes groups are returned as a
// single string instead of multiple attribute values. This delimiter will be
// used split the groups string.
GroupsDelim
string
`json:"groupsDelim"`
RedirectURI
string
`json:"redirectURI"`
// Requested format of the NameID. The NameID value is is mapped to the ID Token
// 'sub' claim.
//
// This can be an abbreviated form of the full URI with just the last component. For
// example, if this value is set to "emailAddress" the format will resolve to:
//
// urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
//
// If no value is specified, this value defaults to:
//
// urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
//
NameIDPolicyFormat
string
`json:"nameIDPolicyFormat"`
}
type
certStore
struct
{
certs
[]
*
x509
.
Certificate
}
func
(
c
certStore
)
Certificates
()
(
roots
[]
*
x509
.
Certificate
,
err
error
)
{
return
c
.
certs
,
nil
}
// Open validates the config and returns a connector. It does not actually
// validate connectivity with the provider.
func
(
c
*
Config
)
Open
(
logger
logrus
.
FieldLogger
)
(
connector
.
Connector
,
error
)
{
return
c
.
openConnector
(
logger
)
}
func
(
c
*
Config
)
openConnector
(
logger
logrus
.
FieldLogger
)
(
interface
{
connector
.
SAMLConnector
},
error
)
{
requiredFields
:=
[]
struct
{
name
,
val
string
}{
{
"issuer"
,
c
.
Issuer
},
{
"ssoURL"
,
c
.
SSOURL
},
{
"usernameAttr"
,
c
.
UsernameAttr
},
{
"emailAttr"
,
c
.
EmailAttr
},
{
"redirectURI"
,
c
.
RedirectURI
},
}
var
missing
[]
string
for
_
,
f
:=
range
requiredFields
{
if
f
.
val
==
""
{
missing
=
append
(
missing
,
f
.
name
)
}
}
switch
len
(
missing
)
{
case
0
:
case
1
:
return
nil
,
fmt
.
Errorf
(
"missing required field %q"
,
missing
[
0
])
default
:
return
nil
,
fmt
.
Errorf
(
"missing required fields %q"
,
missing
)
}
p
:=
&
provider
{
issuer
:
c
.
Issuer
,
ssoURL
:
c
.
SSOURL
,
now
:
time
.
Now
,
usernameAttr
:
c
.
UsernameAttr
,
emailAttr
:
c
.
EmailAttr
,
groupsAttr
:
c
.
GroupsAttr
,
groupsDelim
:
c
.
GroupsDelim
,
redirectURI
:
c
.
RedirectURI
,
logger
:
logger
,
nameIDPolicyFormat
:
c
.
NameIDPolicyFormat
,
}
if
p
.
nameIDPolicyFormat
==
""
{
p
.
nameIDPolicyFormat
=
nameIDFormatPersistent
}
else
{
if
format
,
ok
:=
nameIDFormatLookup
[
p
.
nameIDPolicyFormat
];
ok
{
p
.
nameIDPolicyFormat
=
format
}
else
{
return
nil
,
fmt
.
Errorf
(
"invalid nameIDPolicyFormat: %q"
,
p
.
nameIDPolicyFormat
)
}
}
if
!
c
.
InsecureSkipSignatureValidation
{
if
(
c
.
CA
==
""
)
==
(
c
.
CAData
==
nil
)
{
return
nil
,
errors
.
New
(
"must provide either 'ca' or 'caData'"
)
}
var
caData
[]
byte
if
c
.
CA
!=
""
{
data
,
err
:=
ioutil
.
ReadFile
(
c
.
CA
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"read ca file: %v"
,
err
)
}
caData
=
data
}
else
{
caData
=
c
.
CAData
}
var
(
certs
[]
*
x509
.
Certificate
block
*
pem
.
Block
)
for
{
block
,
caData
=
pem
.
Decode
(
caData
)
if
block
==
nil
{
break
}
cert
,
err
:=
x509
.
ParseCertificate
(
block
.
Bytes
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"parse cert: %v"
,
err
)
}
certs
=
append
(
certs
,
cert
)
}
if
len
(
certs
)
==
0
{
return
nil
,
errors
.
New
(
"no certificates found in ca data"
)
}
p
.
validator
=
dsig
.
NewDefaultValidationContext
(
certStore
{
certs
})
}
return
p
,
nil
}
type
provider
struct
{
issuer
string
ssoURL
string
now
func
()
time
.
Time
// If nil, don't do signature validation.
validator
*
dsig
.
ValidationContext
// Attribute mappings
usernameAttr
string
emailAttr
string
groupsAttr
string
groupsDelim
string
redirectURI
string
nameIDPolicyFormat
string
logger
logrus
.
FieldLogger
}
func
(
p
*
provider
)
POSTData
(
s
connector
.
Scopes
)
(
action
,
value
string
,
err
error
)
{
// NOTE(ericchiang): If we can't follow up with the identity provider, can we
// support refresh tokens?
if
s
.
OfflineAccess
{
return
""
,
""
,
fmt
.
Errorf
(
"SAML does not support offline access"
)
}
r
:=
&
authnRequest
{
ProtocolBinding
:
bindingPOST
,
ID
:
"_"
+
uuidv4
(),
IssueInstant
:
xmlTime
(
p
.
now
()),
Destination
:
p
.
ssoURL
,
Issuer
:
&
issuer
{
Issuer
:
p
.
issuer
,
},
NameIDPolicy
:
&
nameIDPolicy
{
AllowCreate
:
true
,
Format
:
p
.
nameIDPolicyFormat
,
},
}
data
,
err
:=
xml
.
MarshalIndent
(
r
,
""
,
" "
)
if
err
!=
nil
{
return
""
,
""
,
fmt
.
Errorf
(
"marshal authn request: %v"
,
err
)
}
buff
:=
new
(
bytes
.
Buffer
)
fw
,
err
:=
flate
.
NewWriter
(
buff
,
flate
.
DefaultCompression
)
if
err
!=
nil
{
return
""
,
""
,
fmt
.
Errorf
(
"new flate writer: %v"
,
err
)
}
if
_
,
err
:=
fw
.
Write
(
data
);
err
!=
nil
{
return
""
,
""
,
fmt
.
Errorf
(
"compress message: %v"
,
err
)
}
if
err
:=
fw
.
Close
();
err
!=
nil
{
return
""
,
""
,
fmt
.
Errorf
(
"flush message: %v"
,
err
)
}
return
p
.
ssoURL
,
base64
.
StdEncoding
.
EncodeToString
(
buff
.
Bytes
()),
nil
}
func
(
p
*
provider
)
HandlePOST
(
s
connector
.
Scopes
,
samlResponse
string
)
(
ident
connector
.
Identity
,
err
error
)
{
rawResp
,
err
:=
base64
.
StdEncoding
.
DecodeString
(
samlResponse
)
if
err
!=
nil
{
return
ident
,
fmt
.
Errorf
(
"decode response: %v"
,
err
)
}
if
p
.
validator
!=
nil
{
if
rawResp
,
err
=
verify
(
p
.
validator
,
rawResp
);
err
!=
nil
{
return
ident
,
fmt
.
Errorf
(
"verify signature: %v"
,
err
)
}
}
var
resp
response
if
err
:=
xml
.
Unmarshal
(
rawResp
,
&
resp
);
err
!=
nil
{
return
ident
,
fmt
.
Errorf
(
"unmarshal response: %v"
,
err
)
}
if
resp
.
Destination
!=
""
&&
resp
.
Destination
!=
p
.
redirectURI
{
return
ident
,
fmt
.
Errorf
(
"expected destination %q got %q"
,
p
.
redirectURI
,
resp
.
Destination
)
}
assertion
:=
resp
.
Assertion
if
assertion
==
nil
{
return
ident
,
fmt
.
Errorf
(
"response did not contain an assertion"
)
}
subject
:=
assertion
.
Subject
if
subject
==
nil
{
return
ident
,
fmt
.
Errorf
(
"response did not contain a subject"
)
}
switch
{
case
subject
.
NameID
!=
nil
:
if
ident
.
UserID
=
subject
.
NameID
.
Value
;
ident
.
UserID
==
""
{
return
ident
,
fmt
.
Errorf
(
"NameID element does not contain a value"
)
}
default
:
return
ident
,
fmt
.
Errorf
(
"subject does not contain an NameID element"
)
}
attributes
:=
assertion
.
AttributeStatement
if
attributes
==
nil
{
return
ident
,
fmt
.
Errorf
(
"response did not contain a AttributeStatement"
)
}
if
ident
.
Email
,
_
=
attributes
.
get
(
p
.
emailAttr
);
ident
.
Email
==
""
{
return
ident
,
fmt
.
Errorf
(
"no attribute with name %q"
,
p
.
emailAttr
)
}
ident
.
EmailVerified
=
true
if
ident
.
Username
,
_
=
attributes
.
get
(
p
.
usernameAttr
);
ident
.
Username
==
""
{
return
ident
,
fmt
.
Errorf
(
"no attribute with name %q"
,
p
.
usernameAttr
)
}
if
s
.
Groups
&&
p
.
groupsAttr
!=
""
{
if
p
.
groupsDelim
!=
""
{
groupsStr
,
ok
:=
attributes
.
get
(
p
.
groupsAttr
)
if
!
ok
{
return
ident
,
fmt
.
Errorf
(
"no attribute with name %q"
,
p
.
groupsAttr
)
}
// TOOD(ericchiang): Do we need to further trim whitespace?
ident
.
Groups
=
strings
.
Split
(
groupsStr
,
p
.
groupsDelim
)
}
else
{
groups
,
ok
:=
attributes
.
all
(
p
.
groupsAttr
)
if
!
ok
{
return
ident
,
fmt
.
Errorf
(
"no attribute with name %q"
,
p
.
groupsAttr
)
}
ident
.
Groups
=
groups
}
}
return
ident
,
nil
}
// verify checks the signature info of a XML document and returns
// the signed elements.
func
verify
(
validator
*
dsig
.
ValidationContext
,
data
[]
byte
)
(
signed
[]
byte
,
err
error
)
{
doc
:=
etree
.
NewDocument
()
if
err
:=
doc
.
ReadFromBytes
(
data
);
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"parse document: %v"
,
err
)
}
result
,
err
:=
validator
.
Validate
(
doc
.
Root
())
if
err
!=
nil
{
return
nil
,
err
}
doc
.
SetRoot
(
result
)
return
doc
.
WriteToBytes
()
}
func
uuidv4
()
string
{
u
:=
make
([]
byte
,
16
)
if
_
,
err
:=
rand
.
Read
(
u
);
err
!=
nil
{
panic
(
err
)
}
u
[
6
]
=
(
u
[
6
]
|
0x40
)
&
0x4F
u
[
8
]
=
(
u
[
8
]
|
0x80
)
&
0xBF
r
:=
make
([]
byte
,
36
)
r
[
8
]
=
'-'
r
[
13
]
=
'-'
r
[
18
]
=
'-'
r
[
23
]
=
'-'
hex
.
Encode
(
r
,
u
[
0
:
4
])
hex
.
Encode
(
r
[
9
:
],
u
[
4
:
6
])
hex
.
Encode
(
r
[
14
:
],
u
[
6
:
8
])
hex
.
Encode
(
r
[
19
:
],
u
[
8
:
10
])
hex
.
Encode
(
r
[
24
:
],
u
[
10
:
])
return
string
(
r
)
}
connector/saml/saml_test.go
0 → 100644
View file @
31dfb54b
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
)
}
}
connector/saml/testdata/okta-ca.pem
0 → 100644
View file @
31dfb54b
-----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-----
connector/saml/testdata/okta-resp.xml
0 → 100644
View file @
31dfb54b
<?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>
connector/saml/types.go
0 → 100644
View file @
31dfb54b
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"`
}
glide.yaml
View file @
31dfb54b
...
...
@@ -131,3 +131,11 @@ import:
version
:
v0.11.0
-
package
:
golang.org/x/sys/unix
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
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment