Skip to content

Commit 0abaef2

Browse files
committed
sso: implement config and provider
1 parent a84b821 commit 0abaef2

21 files changed

+919
-49
lines changed

Makefile

+4-2
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,12 @@ clean:
8888

8989
qtest: covdir
9090
@echo "Perform quick tests ..."
91+
@time richgo test -v -coverprofile=.coverage/coverage.out internal/tag/*.go
9192
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/util/data/...
9293
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestNewConfig ./*.go
9394
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestNewServer ./*.go
9495
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/registry/...
9596
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/waf/...
96-
@#time richgo test -v -coverprofile=.coverage/coverage.out internal/tag/*.go
9797
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestAuthorize ./pkg/authz/validator/...
9898
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestAddProviders ./pkg/messaging/...
9999
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/messaging/...
@@ -103,7 +103,9 @@ qtest: covdir
103103
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/authn/...
104104
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestNewPortal ./pkg/authn/*.go
105105
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestServeHTTP ./pkg/authn/*.go
106-
@time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestFactory ./pkg/authn/cookie/*.go
106+
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestFactory ./pkg/authn/cookie/*.go
107+
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestNewSingleSignOnProviderConfig ./pkg/sso/*.go
108+
@time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestNewSingleSignOnProvider ./pkg/sso/*.go
107109
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestValidateJwksKey ./pkg/authn/backends/oauth2/jwks*.go
108110
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestTransformData ./pkg/authn/transformer/*.go
109111
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/authn/icons/...

assets/portal/templates/basic/apps_aws_sso.template assets/portal/templates/basic/apps_sso.template

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<link rel="icon" href="{{ pathjoin .ActionEndpoint "/assets/images/favicon.png" }}" type="image/png" />
1212
<link rel="stylesheet" href="{{ pathjoin .ActionEndpoint "/assets/google-webfonts/roboto.css" }}" />
1313
<link rel="stylesheet" href="{{ pathjoin .ActionEndpoint "/assets/line-awesome/line-awesome.css" }}" />
14-
<link rel="stylesheet" href="{{ pathjoin .ActionEndpoint "/assets/css/apps_aws_sso.css" }}" />
14+
<link rel="stylesheet" href="{{ pathjoin .ActionEndpoint "/assets/css/apps_sso.css" }}" />
1515
{{ if eq .Data.ui_options.custom_css_required "yes" }}
1616
<link rel="stylesheet" href="{{ pathjoin .ActionEndpoint "/assets/css/custom.css" }}" />
1717
{{ end }}
@@ -86,7 +86,7 @@
8686
</div>
8787
</div>
8888
<!-- JavaScript -->
89-
<script src="{{ pathjoin .ActionEndpoint "/assets/js/apps_aws_sso.js" }}"></script>
89+
<script src="{{ pathjoin .ActionEndpoint "/assets/js/apps_sso.js" }}"></script>
9090
{{ if eq .Data.ui_options.custom_js_required "yes" }}
9191
<script src="{{ pathjoin .ActionEndpoint "/assets/js/custom.js" }}"></script>
9292
{{ end }}

assets/portal/templates/basic/login.template

+4-4
Original file line numberDiff line numberDiff line change
@@ -84,22 +84,22 @@
8484
</form>
8585
</div>
8686

87-
<div id="user_actions" class="flex flex-wrap pt-6 justify-center gap-4{{ if or (ne $authenticatorCount 1) (eq .Data.login_options.hide_links "yes") }} hidden{{ end -}}">
88-
<div id="user_register_link"{{ if eq .Data.login_options.hide_register_link "yes" }} class="hidden"{{ end -}}>
87+
<div id="user_actions" class="flex flex-wrap pt-6 justify-center gap-4 {{ if or (ne $authenticatorCount 1) (eq .Data.login_options.hide_links "yes") }}hidden{{ end -}}">
88+
<div id="user_register_link" {{ if eq .Data.login_options.hide_register_link "yes" }}class="hidden"{{ end -}}>
8989
<a class="text-primary-600" href="{{ pathjoin .ActionEndpoint "/register" .Data.login_options.default_realm }}">
9090
<i class="las la-book"></i>
9191
<span class="text-lg">Register</span>
9292
</a>
9393
</div>
9494

95-
<div id="forgot_username_link"{{ if eq .Data.login_options.hide_forgot_username_link "yes" }} class="hidden"{{ end -}}>
95+
<div id="forgot_username_link" {{ if eq .Data.login_options.hide_forgot_username_link "yes" }}class="hidden"{{ end -}}>
9696
<a class="text-primary-600" href="{{ pathjoin .ActionEndpoint "/forgot" .Data.login_options.default_realm }}">
9797
<i class="las la-unlock"></i>
9898
<span class="text-lg">Forgot Username?</span>
9999
</a>
100100
</div>
101101

102-
<div id="contact_support_link"{{ if eq .Data.login_options.hide_contact_support_link "yes" }} class="hidden"{{ end -}}>
102+
<div id="contact_support_link" {{ if eq .Data.login_options.hide_contact_support_link "yes" }}class="hidden"{{ end -}}>
103103
<a class="text-primary-600" href="{{ pathjoin .ActionEndpoint "/help" .Data.login_options.default_realm }}">
104104
<i class="las la-info-circle"></i>
105105
<span class="text-lg">Contact Support</span>

assets/scripts/generate_ui.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ _PAGES[${#_PAGES[@]}]="register"
1616
_PAGES[${#_PAGES[@]}]="generic"
1717
_PAGES[${#_PAGES[@]}]="settings"
1818
_PAGES[${#_PAGES[@]}]="sandbox"
19-
_PAGES[${#_PAGES[@]}]="apps_aws_sso"
19+
_PAGES[${#_PAGES[@]}]="apps_sso"
2020
_PAGES[${#_PAGES[@]}]="apps_mobile_access"
2121

2222
printf "package ui\n\n" > ${UI_FILE}

config.go

+32-6
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,18 @@ import (
2424
"github.com/greenpau/go-authcrunch/pkg/ids"
2525
"github.com/greenpau/go-authcrunch/pkg/messaging"
2626
"github.com/greenpau/go-authcrunch/pkg/registry"
27+
"github.com/greenpau/go-authcrunch/pkg/sso"
2728
)
2829

2930
// Config is a configuration of Server.
3031
type Config struct {
31-
Credentials *credentials.Config `json:"credentials,omitempty" xml:"credentials,omitempty" yaml:"credentials,omitempty"`
32-
Messaging *messaging.Config `json:"messaging,omitempty" xml:"messaging,omitempty" yaml:"messaging,omitempty"`
33-
AuthenticationPortals []*authn.PortalConfig `json:"authentication_portals,omitempty" xml:"authentication_portals,omitempty" yaml:"authentication_portals,omitempty"`
34-
AuthorizationPolicies []*authz.PolicyConfig `json:"authorization_policies,omitempty" xml:"authorization_policies,omitempty" yaml:"authorization_policies,omitempty"`
35-
IdentityStores []*ids.IdentityStoreConfig `json:"identity_stores,omitempty" xml:"identity_stores,omitempty" yaml:"identity_stores,omitempty"`
36-
IdentityProviders []*idp.IdentityProviderConfig `json:"identity_providers,omitempty" xml:"identity_providers,omitempty" yaml:"identity_providers,omitempty"`
32+
Credentials *credentials.Config `json:"credentials,omitempty" xml:"credentials,omitempty" yaml:"credentials,omitempty"`
33+
Messaging *messaging.Config `json:"messaging,omitempty" xml:"messaging,omitempty" yaml:"messaging,omitempty"`
34+
AuthenticationPortals []*authn.PortalConfig `json:"authentication_portals,omitempty" xml:"authentication_portals,omitempty" yaml:"authentication_portals,omitempty"`
35+
AuthorizationPolicies []*authz.PolicyConfig `json:"authorization_policies,omitempty" xml:"authorization_policies,omitempty" yaml:"authorization_policies,omitempty"`
36+
IdentityStores []*ids.IdentityStoreConfig `json:"identity_stores,omitempty" xml:"identity_stores,omitempty" yaml:"identity_stores,omitempty"`
37+
IdentityProviders []*idp.IdentityProviderConfig `json:"identity_providers,omitempty" xml:"identity_providers,omitempty" yaml:"identity_providers,omitempty"`
38+
SingleSignOnProviders []*sso.SingleSignOnProviderConfig `json:"sso_providers,omitempty" xml:"sso_providers,omitempty" yaml:"sso_providers,omitempty"`
3739
disabledIdentityStores map[string]interface{}
3840
disabledIdentityProviders map[string]interface{}
3941
UserRegistries []*registry.UserRegistryConfig `json:"user_registries,omitempty" xml:"user_registries,omitempty" yaml:"user_registries,omitempty"`
@@ -80,6 +82,16 @@ func (cfg *Config) AddIdentityProvider(name, kind string, data map[string]interf
8082
return nil
8183
}
8284

85+
// AddSingleSignOnProvider adds a single sign-on provider configuration.
86+
func (cfg *Config) AddSingleSignOnProvider(data map[string]interface{}) error {
87+
provider, err := sso.NewSingleSignOnProviderConfig(data)
88+
if err != nil {
89+
return err
90+
}
91+
cfg.SingleSignOnProviders = append(cfg.SingleSignOnProviders, provider)
92+
return nil
93+
}
94+
8395
// AddAuthenticationPortal adds an authentication portal configuration.
8496
func (cfg *Config) AddAuthenticationPortal(p *authn.PortalConfig) error {
8597
if err := p.Validate(); err != nil {
@@ -230,6 +242,20 @@ func (cfg *Config) Validate() error {
230242
authByName[providerName] = "identity provider in " + realmName + " realm"
231243
}
232244
}
245+
246+
// Iterate over SSO providers.
247+
for _, providerName := range portalCfg.SingleSignOnProviders {
248+
var providerFound bool
249+
for _, entry := range cfg.SingleSignOnProviders {
250+
if providerName == entry.Name {
251+
providerFound = true
252+
break
253+
}
254+
}
255+
if !providerFound {
256+
return fmt.Errorf("sso provider %q configuration not found", providerName)
257+
}
258+
}
233259
}
234260

235261
return nil

internal/tag/tag_test.go

+24-2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import (
4747
"github.com/greenpau/go-authcrunch/pkg/messaging"
4848
"github.com/greenpau/go-authcrunch/pkg/registry"
4949
"github.com/greenpau/go-authcrunch/pkg/requests"
50+
"github.com/greenpau/go-authcrunch/pkg/sso"
5051
"github.com/greenpau/go-authcrunch/pkg/user"
5152
"github.com/greenpau/go-authcrunch/pkg/util"
5253
"github.com/greenpau/go-authcrunch/pkg/util/cfg"
@@ -66,6 +67,16 @@ func TestTagCompliance(t *testing.T) {
6667
shouldErr bool
6768
err error
6869
}{
70+
{
71+
name: "test sso.Provider struct",
72+
entry: &sso.Provider{},
73+
opts: &Options{},
74+
},
75+
{
76+
name: "test sso.SingleSignOnProviderConfig struct",
77+
entry: &sso.SingleSignOnProviderConfig{},
78+
opts: &Options{},
79+
},
6980
{
7081
name: "test ui.NavigationItem struct",
7182
entry: &ui.NavigationItem{},
@@ -104,7 +115,12 @@ func TestTagCompliance(t *testing.T) {
104115
{
105116
name: "test authn.PortalParameters struct",
106117
entry: &authn.PortalParameters{},
107-
opts: &Options{},
118+
opts: &Options{
119+
AllowFieldMismatch: true,
120+
AllowedFields: map[string]interface{}{
121+
"sso_providers": true,
122+
},
123+
},
108124
},
109125
{
110126
name: "test idp.IdentityProviderConfig struct",
@@ -386,7 +402,12 @@ func TestTagCompliance(t *testing.T) {
386402
{
387403
name: "test authn.PortalConfig struct",
388404
entry: &authn.PortalConfig{},
389-
opts: &Options{},
405+
opts: &Options{
406+
AllowFieldMismatch: true,
407+
AllowedFields: map[string]interface{}{
408+
"sso_providers": true,
409+
},
410+
},
390411
},
391412
{
392413
name: "test requests.AuthorizationRequest struct",
@@ -622,6 +643,7 @@ func TestTagCompliance(t *testing.T) {
622643
AllowedFields: map[string]interface{}{
623644
"auth_portal_configs": true,
624645
"authz_policy_configs": true,
646+
"sso_providers": true,
625647
},
626648
},
627649
},

pkg/authn/config.go

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ type PortalConfig struct {
3939
IdentityStores []string `json:"identity_stores,omitempty" xml:"identity_stores,omitempty" yaml:"identity_stores,omitempty"`
4040
// The names of identity providers.
4141
IdentityProviders []string `json:"identity_providers,omitempty" xml:"identity_providers,omitempty" yaml:"identity_providers,omitempty"`
42+
// The names of SSO providers.
43+
SingleSignOnProviders []string `json:"sso_providers,omitempty" xml:"sso_providers,omitempty" yaml:"sso_providers,omitempty"`
4244
// The names of user registries.
4345
UserRegistries []string `json:"user_registries,omitempty" xml:"user_registries,omitempty" yaml:"user_registries,omitempty"`
4446
// AccessListConfigs hold the configurations for the ACL of the token validator.

pkg/authn/handle_http_apps_aws_sso.go pkg/authn/handle_http_apps_sso.go

+57-2
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,18 @@ package authn
1616

1717
import (
1818
"context"
19+
"fmt"
1920
"github.com/greenpau/go-authcrunch/pkg/requests"
2021
"github.com/greenpau/go-authcrunch/pkg/user"
2122
"go.uber.org/zap"
2223
"net/http"
2324
"strings"
2425
)
2526

26-
func (p *Portal) handleHTTPAppsAwsSso(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request, parsedUser *user.User) error {
27+
func (p *Portal) handleHTTPAppsSingleSignOn(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request, parsedUser *user.User) error {
28+
var assumedRole, authorizedRole bool
29+
var accountID, roleName string
30+
2731
p.disableClientCache(w)
2832
p.injectRedirectURL(ctx, w, r, rr)
2933

@@ -54,6 +58,30 @@ func (p *Portal) handleHTTPAppsAwsSso(ctx context.Context, w http.ResponseWriter
5458
resp.PageTitle = "AWS SSO"
5559
resp.BaseURL(rr.Upstream.BasePath)
5660

61+
if strings.Contains(r.URL.Path, "/apps/sso") && strings.Contains(r.URL.Path, "metadata.xml") {
62+
// TODO(greenpau): add metadata for realm.
63+
}
64+
65+
if strings.Contains(r.URL.Path, "/apps/sso/assume") {
66+
accountRole, err := getEndpoint(r.URL.Path, "/apps/sso/assume/")
67+
if err != nil {
68+
p.logger.Warn(
69+
"SSO request failed",
70+
zap.String("session_id", rr.Upstream.SessionID),
71+
zap.String("request_id", rr.ID),
72+
zap.String("error", "malformed SSO request"),
73+
)
74+
} else {
75+
assumedRole = true
76+
arr := strings.SplitN(accountRole, "/", 2)
77+
if len(arr) != 2 {
78+
return p.handleHTTPRenderError(ctx, w, r, rr, fmt.Errorf("Malformed SSO request"))
79+
}
80+
accountID = arr[0]
81+
roleName = arr[1]
82+
}
83+
}
84+
5785
type roleEntry struct {
5886
Name string
5987
AccountID string
@@ -73,12 +101,39 @@ func (p *Portal) handleHTTPAppsAwsSso(ctx context.Context, w http.ResponseWriter
73101
AccountID: arr[1],
74102
}
75103
roles = append(roles, role)
104+
105+
if assumedRole {
106+
if (role.Name == roleName) && (role.AccountID == accountID) {
107+
authorizedRole = true
108+
p.logger.Debug(
109+
"SSO assume role request received",
110+
zap.String("session_id", rr.Upstream.SessionID),
111+
zap.String("request_id", rr.ID),
112+
zap.String("role_name", roleName),
113+
zap.String("account_id", accountID),
114+
)
115+
}
116+
}
117+
}
118+
119+
if assumedRole {
120+
if !authorizedRole {
121+
p.logger.Debug(
122+
"Unauthorized SSO assume role request",
123+
zap.String("session_id", rr.Upstream.SessionID),
124+
zap.String("request_id", rr.ID),
125+
zap.String("role_name", roleName),
126+
zap.String("account_id", accountID),
127+
)
128+
return p.handleHTTPRenderError(ctx, w, r, rr, fmt.Errorf("Unauthorized SSO assume role request"))
129+
}
130+
p.logger.Debug("Redirecting to SAML endpoint")
76131
}
77132

78133
resp.Data["role_count"] = len(roles)
79134
resp.Data["roles"] = roles
80135

81-
content, err := p.ui.Render("apps_aws_sso", resp)
136+
content, err := p.ui.Render("apps_sso", resp)
82137
if err != nil {
83138
return p.handleHTTPRenderError(ctx, w, r, rr, err)
84139
}

pkg/authn/portal.go

+28-4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/greenpau/go-authcrunch/pkg/ids"
3030
"github.com/greenpau/go-authcrunch/pkg/kms"
3131
"github.com/greenpau/go-authcrunch/pkg/registry"
32+
"github.com/greenpau/go-authcrunch/pkg/sso"
3233
cfgutil "github.com/greenpau/go-authcrunch/pkg/util/cfg"
3334
"sort"
3435

@@ -54,6 +55,7 @@ type Portal struct {
5455
keystore *kms.CryptoKeyStore
5556
identityStores []ids.IdentityStore
5657
identityProviders []idp.IdentityProvider
58+
ssoProviders []sso.SingleSignOnProvider
5759
cookie *cookie.Factory
5860
transformer *transformer.Factory
5961
ui *ui.Factory
@@ -66,10 +68,11 @@ type Portal struct {
6668

6769
// PortalParameters are input parameters for NewPortal.
6870
type PortalParameters struct {
69-
Config *PortalConfig `json:"config,omitempty" xml:"config,omitempty" yaml:"config,omitempty"`
70-
Logger *zap.Logger `json:"logger,omitempty" xml:"logger,omitempty" yaml:"logger,omitempty"`
71-
IdentityStores []ids.IdentityStore `json:"identity_stores,omitempty" xml:"identity_stores,omitempty" yaml:"identity_stores,omitempty"`
72-
IdentityProviders []idp.IdentityProvider `json:"identity_providers,omitempty" xml:"identity_providers,omitempty" yaml:"identity_providers,omitempty"`
71+
Config *PortalConfig `json:"config,omitempty" xml:"config,omitempty" yaml:"config,omitempty"`
72+
Logger *zap.Logger `json:"logger,omitempty" xml:"logger,omitempty" yaml:"logger,omitempty"`
73+
IdentityStores []ids.IdentityStore `json:"identity_stores,omitempty" xml:"identity_stores,omitempty" yaml:"identity_stores,omitempty"`
74+
IdentityProviders []idp.IdentityProvider `json:"identity_providers,omitempty" xml:"identity_providers,omitempty" yaml:"identity_providers,omitempty"`
75+
SingleSignOnProviders []sso.SingleSignOnProvider `json:"sso_providers,omitempty" xml:"sso_providers,omitempty" yaml:"sso_providers,omitempty"`
7376
}
7477

7578
// NewPortal returns an instance of Portal.
@@ -132,6 +135,27 @@ func NewPortal(params PortalParameters) (*Portal, error) {
132135
}
133136
}
134137

138+
for _, providerName := range params.Config.SingleSignOnProviders {
139+
var providerFound bool
140+
for _, provider := range params.SingleSignOnProviders {
141+
if provider.GetName() == providerName {
142+
if !provider.Configured() {
143+
return nil, errors.ErrNewPortal.WithArgs(
144+
fmt.Errorf("sso provider %q not configured", providerName),
145+
)
146+
}
147+
p.ssoProviders = append(p.ssoProviders, provider)
148+
providerFound = true
149+
break
150+
}
151+
}
152+
if !providerFound {
153+
return nil, errors.ErrNewPortal.WithArgs(
154+
fmt.Errorf("sso provider %q not found", providerName),
155+
)
156+
}
157+
}
158+
135159
if len(p.identityStores) < 1 && len(p.identityProviders) < 1 {
136160
return nil, errors.ErrNewPortal.WithArgs(errors.ErrPortalConfigBackendsNotFound)
137161
}

pkg/authn/respond_http.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,13 @@ func (p *Portal) handleHTTP(ctx context.Context, w http.ResponseWriter, r *http.
4747
return p.handleHTTPRegister(ctx, w, r, rr)
4848
case strings.HasSuffix(r.URL.Path, "/whoami"):
4949
return p.handleHTTPWhoami(ctx, w, r, rr, usr)
50-
case strings.Contains(r.URL.Path, "/apps/aws-sso"):
51-
return p.handleHTTPAppsAwsSso(ctx, w, r, rr, usr)
50+
case strings.Contains(r.URL.Path, "/apps/sso"):
51+
return p.handleHTTPAppsSingleSignOn(ctx, w, r, rr, usr)
5252
case strings.Contains(r.URL.Path, "/apps/mobile-access"):
5353
return p.handleHTTPAppsMobileAccess(ctx, w, r, rr, usr)
5454
case strings.Contains(r.URL.Path, "/oauth2/") && strings.HasSuffix(r.URL.Path, "/logout"):
5555
return p.handleHTTPExternalLogout(ctx, w, r, rr, "oauth2")
5656
case strings.Contains(r.URL.Path, "/saml/"):
57-
// TODO(greenpau): implement
58-
// p.logRequest("external saml login traceback", r, rr)
5957
return p.handleHTTPExternalLogin(ctx, w, r, rr, "saml")
6058
case strings.Contains(r.URL.Path, "/oauth2/"):
6159
return p.handleHTTPExternalLogin(ctx, w, r, rr, "oauth2")

pkg/authn/ui/content.go

+10-10
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)