Skip to content

Commit ff00023

Browse files
committed
sso: implement metadata.xml handler
1 parent c8cde84 commit ff00023

File tree

7 files changed

+374
-16
lines changed

7 files changed

+374
-16
lines changed

Makefile

+2-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ qtest: covdir
106106
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestFactory ./pkg/authn/cookie/*.go
107107
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestNewSingleSignOnProviderConfig ./pkg/sso/*.go
108108
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestNewSingleSignOnProvider ./pkg/sso/*.go
109-
@time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestParseRequestURL ./pkg/sso/request*.go
109+
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestParseRequestURL ./pkg/sso/request*.go
110+
@time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestGetMetadata ./pkg/sso/*.go
110111
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestValidateJwksKey ./pkg/authn/backends/oauth2/jwks*.go
111112
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestTransformData ./pkg/authn/transformer/*.go
112113
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/authn/icons/...

internal/tag/tag_test.go

+56
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,62 @@ func TestTagCompliance(t *testing.T) {
6767
shouldErr bool
6868
err error
6969
}{
70+
{
71+
name: "test sso.KeyInfo struct",
72+
entry: &sso.KeyInfo{},
73+
opts: &Options{
74+
Disabled: true,
75+
},
76+
},
77+
{
78+
name: "test sso.SingleSignOnService struct",
79+
entry: &sso.SingleSignOnService{},
80+
opts: &Options{
81+
Disabled: true,
82+
},
83+
},
84+
{
85+
name: "test sso.EntityDescriptor struct",
86+
entry: &sso.EntityDescriptor{},
87+
opts: &Options{
88+
Disabled: true,
89+
},
90+
},
91+
{
92+
name: "test sso.X509Data struct",
93+
entry: &sso.X509Data{},
94+
opts: &Options{
95+
Disabled: true,
96+
},
97+
},
98+
{
99+
name: "test sso.IDPEntityDescriptor struct",
100+
entry: &sso.IDPEntityDescriptor{},
101+
opts: &Options{
102+
Disabled: true,
103+
},
104+
},
105+
{
106+
name: "test sso.Service struct",
107+
entry: &sso.Service{},
108+
opts: &Options{
109+
Disabled: true,
110+
},
111+
},
112+
{
113+
name: "test sso.IDPSSODescriptor struct",
114+
entry: &sso.IDPSSODescriptor{},
115+
opts: &Options{
116+
Disabled: true,
117+
},
118+
},
119+
{
120+
name: "test sso.KeyDescriptor struct",
121+
entry: &sso.KeyDescriptor{},
122+
opts: &Options{
123+
Disabled: true,
124+
},
125+
},
70126
{
71127
name: "test sso.Provider struct",
72128
entry: &sso.Provider{},

pkg/authn/handle_http_apps_sso.go

+8-6
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,13 @@ func (p *Portal) handleHTTPAppsSingleSignOn(ctx context.Context, w http.Response
8787
// handleHTTPAppsSingleSignOnMetadata renders metadata.xml content. It is only available to admin users.
8888
func (p *Portal) handleHTTPAppsSingleSignOnMetadata(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request,
8989
provider sso.SingleSignOnProvider, roles []*assumeRoleEntry) error {
90-
// body := []byte("METADATA")
91-
w.Header().Set("Content-Type", "text/html")
90+
metadata, err := provider.GetMetadata()
91+
if err != nil {
92+
return p.handleHTTPRenderError(ctx, w, r, rr, err)
93+
}
94+
w.Header().Set("Content-Type", "application/xml")
9295
w.WriteHeader(http.StatusOK)
93-
// w.Write(body)
94-
w.Write(provider.GetMetadata())
96+
w.Write(metadata)
9597
return nil
9698
}
9799

@@ -200,8 +202,8 @@ func fetchSingleSignOnRoles(providerName string, usr *user.User) []*assumeRoleEn
200202
continue
201203
}
202204
role := &assumeRoleEntry{
203-
Name: arr[2],
204-
AccountID: arr[1],
205+
Name: arr[2],
206+
AccountID: arr[1],
205207
ProviderName: providerName,
206208
}
207209
roles = append(roles, role)

pkg/sso/metadata.go

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Copyright 2022 Paul Greenberg [email protected]
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package sso
16+
17+
import (
18+
"bytes"
19+
"encoding/base64"
20+
"encoding/xml"
21+
)
22+
23+
// EntityDescriptor TODO.
24+
type EntityDescriptor struct {
25+
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata EntityDescriptor"`
26+
ID string `xml:",attr,omitempty"`
27+
EntityID string `xml:"entityID,attr"`
28+
}
29+
30+
// X509Data TODO.
31+
type X509Data struct {
32+
XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# X509Data"`
33+
X509Certificate string `xml:"http://www.w3.org/2000/09/xmldsig# X509Certificate"`
34+
}
35+
36+
// KeyInfo TODO.
37+
type KeyInfo struct {
38+
XMLName xml.Name `xml:"http://www.w3.org/2000/09/xmldsig# KeyInfo"`
39+
X509Data *X509Data
40+
}
41+
42+
// KeyDescriptor TODO.
43+
type KeyDescriptor struct {
44+
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata KeyDescriptor"`
45+
Use string `xml:"use,attr,omitempty"`
46+
KeyInfo KeyInfo
47+
}
48+
49+
// Service TODO.
50+
type Service struct {
51+
Binding string `xml:",attr"`
52+
Location string `xml:",attr"`
53+
}
54+
55+
// SingleSignOnService TODO.
56+
type SingleSignOnService struct {
57+
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata SingleSignOnService"`
58+
Service
59+
}
60+
61+
// IDPSSODescriptor TODO.
62+
type IDPSSODescriptor struct {
63+
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata IDPSSODescriptor"`
64+
WantAuthnRequestsSigned bool `xml:",attr"`
65+
ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"`
66+
KeyDescriptor KeyDescriptor
67+
NameIDFormat string `xml:"NameIDFormat"`
68+
SingleSignOnService []SingleSignOnService
69+
}
70+
71+
// IDPEntityDescriptor TODO.
72+
type IDPEntityDescriptor struct {
73+
*EntityDescriptor
74+
IDPSSODescriptor *IDPSSODescriptor
75+
}
76+
77+
// GetMetadata returns the contents of metadata.xml.
78+
func (p *Provider) GetMetadata() ([]byte, error) {
79+
if len(p.metadata) > 0 {
80+
return p.metadata, nil
81+
}
82+
entity := &IDPEntityDescriptor{
83+
EntityDescriptor: &EntityDescriptor{
84+
EntityID: p.config.EntityID,
85+
},
86+
IDPSSODescriptor: &IDPSSODescriptor{
87+
ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
88+
KeyDescriptor: KeyDescriptor{
89+
Use: "signing",
90+
KeyInfo: KeyInfo{
91+
X509Data: &X509Data{
92+
X509Certificate: base64.StdEncoding.EncodeToString(p.cert.Raw),
93+
},
94+
},
95+
},
96+
WantAuthnRequestsSigned: false,
97+
NameIDFormat: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
98+
},
99+
}
100+
101+
for _, location := range p.config.Locations {
102+
svc := SingleSignOnService{
103+
Service: Service{
104+
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
105+
Location: location,
106+
},
107+
}
108+
entity.IDPSSODescriptor.SingleSignOnService = append(entity.IDPSSODescriptor.SingleSignOnService, svc)
109+
}
110+
111+
var b bytes.Buffer
112+
b.Write([]byte(xml.Header))
113+
encoder := xml.NewEncoder(&b)
114+
encoder.Indent("", " ")
115+
if err := encoder.Encode(entity); err != nil {
116+
return nil, err
117+
}
118+
output := bytes.ReplaceAll(b.Bytes(), []byte("></SingleSignOnService>"), []byte("/>"))
119+
output = bytes.ReplaceAll(output,
120+
[]byte(`<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"`),
121+
[]byte(`<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"`))
122+
output = bytes.ReplaceAll(output,
123+
[]byte(`<IDPSSODescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"`),
124+
[]byte(`<md:IDPSSODescriptor`))
125+
output = bytes.ReplaceAll(output,
126+
[]byte(`<KeyDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"`),
127+
[]byte(`<md:KeyDescriptor`))
128+
output = bytes.ReplaceAll(output,
129+
[]byte(`<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">`),
130+
[]byte(`<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">`))
131+
output = bytes.ReplaceAll(output,
132+
[]byte(`<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#">`),
133+
[]byte(`<ds:X509Data>`))
134+
output = bytes.ReplaceAll(output,
135+
[]byte(`<X509Certificate xmlns="http://www.w3.org/2000/09/xmldsig#">`),
136+
[]byte(`<ds:X509Certificate>`))
137+
output = bytes.ReplaceAll(output,
138+
[]byte(`SingleSignOnService xmlns="urn:oasis:names:tc:SAML:2.0:metadata"`),
139+
[]byte(`md:SingleSignOnService`))
140+
output = bytes.ReplaceAll(output, []byte(`</IDPSSODescriptor>`), []byte(`</md:IDPSSODescriptor>`))
141+
output = bytes.ReplaceAll(output, []byte(`</EntityDescriptor>`), []byte(`</md:EntityDescriptor>`))
142+
output = bytes.ReplaceAll(output, []byte(`</X509Data>`), []byte(`</ds:X509Data>`))
143+
output = bytes.ReplaceAll(output, []byte(`</KeyInfo>`), []byte(`</ds:KeyInfo>`))
144+
output = bytes.ReplaceAll(output, []byte(`</KeyDescriptor>`), []byte(`</md:KeyDescriptor>`))
145+
output = bytes.ReplaceAll(output, []byte(`</X509Certificate>`), []byte(`</ds:X509Certificate>`))
146+
output = bytes.ReplaceAll(output, []byte(`NameIDFormat>`), []byte(`md:NameIDFormat>`))
147+
p.metadata = output
148+
return p.metadata, nil
149+
}

pkg/sso/metadata_test.go

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright 2022 Paul Greenberg [email protected]
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package sso
16+
17+
import (
18+
"bytes"
19+
"fmt"
20+
"testing"
21+
22+
"github.com/google/go-cmp/cmp"
23+
fileutil "github.com/greenpau/go-authcrunch/pkg/util/file"
24+
logutil "github.com/greenpau/go-authcrunch/pkg/util/log"
25+
"go.uber.org/zap"
26+
)
27+
28+
func TestGetMetadata(t *testing.T) {
29+
testcases := []struct {
30+
name string
31+
config *SingleSignOnProviderConfig
32+
metadataFilePath string
33+
disableLogger bool
34+
want string
35+
shouldErr bool
36+
err error
37+
}{
38+
{
39+
name: "test valid sso provider metadata",
40+
metadataFilePath: "../../testdata/sso/authp_saml_metadata.xml",
41+
config: &SingleSignOnProviderConfig{
42+
Name: "aws",
43+
Driver: "aws",
44+
EntityID: "caddy-authp-idp",
45+
PrivateKeyPath: "../../testdata/sso/authp_saml.key",
46+
CertPath: "../../testdata/sso/authp_saml.crt",
47+
Locations: []string{
48+
"https://localhost/apps/sso/aws",
49+
"https://127.0.0.1/apps/sso/aws",
50+
},
51+
},
52+
want: `{
53+
"name": "aws",
54+
"driver": "aws",
55+
"config": {
56+
"name": "aws",
57+
"driver": "aws",
58+
"entity_id": "caddy-authp-idp",
59+
"private_key_path": "../../testdata/sso/authp_saml.key",
60+
"cert_path": "../../testdata/sso/authp_saml.crt",
61+
"locations": [
62+
"https://localhost/apps/sso/aws",
63+
"https://127.0.0.1/apps/sso/aws"
64+
]
65+
}
66+
}`,
67+
},
68+
}
69+
for _, tc := range testcases {
70+
t.Run(tc.name, func(t *testing.T) {
71+
var logger *zap.Logger
72+
msgs := []string{fmt.Sprintf("test name: %s", tc.name)}
73+
msgs = append(msgs, fmt.Sprintf("config:\n%v", tc.config))
74+
logger = logutil.NewLogger()
75+
provider, err := NewSingleSignOnProvider(tc.config, logger)
76+
if err != nil {
77+
t.Fatalf("failed initializing sso provider: %v", err)
78+
}
79+
80+
want, err := fileutil.ReadFileBytes(tc.metadataFilePath)
81+
if err != nil {
82+
t.Fatalf("failed reading %q file: %v", tc.metadataFilePath, err)
83+
}
84+
want = bytes.TrimSpace(want)
85+
86+
got, err := provider.GetMetadata()
87+
88+
if err != nil {
89+
if !tc.shouldErr {
90+
t.Fatalf("expected success, got: %v", err)
91+
}
92+
if diff := cmp.Diff(err.Error(), tc.err.Error()); diff != "" {
93+
t.Fatalf("unexpected error: %v, want: %v", err, tc.err)
94+
}
95+
return
96+
}
97+
if tc.shouldErr {
98+
t.Fatalf("unexpected success, want: %v", tc.err)
99+
}
100+
101+
if diff := cmp.Diff(want, got); diff != "" {
102+
t.Errorf("provider.GetMetadata() mismatch (-want +got):\n%s", diff)
103+
}
104+
})
105+
}
106+
}

0 commit comments

Comments
 (0)