Skip to content

Commit 5c551d6

Browse files
committed
sso: add sso app handler
1 parent 39f4bc2 commit 5c551d6

File tree

12 files changed

+384
-70
lines changed

12 files changed

+384
-70
lines changed

Makefile

+3-2
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ clean:
8888

8989
qtest: covdir
9090
@echo "Perform quick tests ..."
91-
@time richgo test -v -coverprofile=.coverage/coverage.out internal/tag/*.go
91+
@#time richgo test -v -coverprofile=.coverage/coverage.out internal/tag/*.go
9292
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/util/data/...
9393
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestNewConfig ./*.go
9494
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestNewServer ./*.go
@@ -105,7 +105,8 @@ qtest: covdir
105105
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestServeHTTP ./pkg/authn/*.go
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
108-
@time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestNewSingleSignOnProvider ./pkg/sso/*.go
108+
@#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
109110
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestValidateJwksKey ./pkg/authn/backends/oauth2/jwks*.go
110111
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out -run TestTransformData ./pkg/authn/transformer/*.go
111112
@#time richgo test $(VERBOSE) $(TEST) -coverprofile=.coverage/coverage.out ./pkg/authn/icons/...

assets/portal/templates/basic/apps_sso.template

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434

3535
{{ if gt .Data.role_count 0 }}
3636
<div class="pb-4 pt-4">
37-
<p class="app-inp-lbl">Assume the following roles on the associated AWS accounts.</p>
37+
<p class="app-inp-lbl">Assume any of the following roles on the associated AWS accounts by clicking the name of the role.</p>
3838
</div>
3939

4040
<div class="flex flex-col">
@@ -51,7 +51,7 @@
5151
{{ range .Data.roles }}
5252
<tr>
5353
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-primary-700 sm:pl-6 md:pl-0 leading-none">
54-
<span>{{ brsplitline .Name }}</span>
54+
<a href="{{ pathjoin $.ActionEndpoint "/apps/sso" .ProviderName "assume" .AccountID .Name }}">{{ brsplitline .Name }}</a>
5555
</td>
5656
<td class="whitespace-nowrap py-4 px-3 text-sm text-primary-500">{{ .AccountID }}</td>
5757
</tr>

internal/tag/tag_test.go

+5
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ func TestTagCompliance(t *testing.T) {
7777
entry: &sso.SingleSignOnProviderConfig{},
7878
opts: &Options{},
7979
},
80+
{
81+
name: "test sso.Request struct",
82+
entry: &sso.Request{},
83+
opts: &Options{},
84+
},
8085
{
8186
name: "test ui.NavigationItem struct",
8287
entry: &ui.NavigationItem{},

pkg/authn/handle_http_apps_sso.go

+128-59
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,22 @@ package authn
1717
import (
1818
"context"
1919
"fmt"
20+
"net/http"
21+
"strings"
22+
2023
"github.com/greenpau/go-authcrunch/pkg/requests"
24+
"github.com/greenpau/go-authcrunch/pkg/sso"
2125
"github.com/greenpau/go-authcrunch/pkg/user"
2226
"go.uber.org/zap"
23-
"net/http"
24-
"strings"
2527
)
2628

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
29+
type assumeRoleEntry struct {
30+
Name string
31+
AccountID string
32+
ProviderName string
33+
}
3034

35+
func (p *Portal) handleHTTPAppsSingleSignOn(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request, parsedUser *user.User) error {
3136
p.disableClientCache(w)
3237
p.injectRedirectURL(ctx, w, r, rr)
3338

@@ -54,55 +59,69 @@ func (p *Portal) handleHTTPAppsSingleSignOn(ctx context.Context, w http.Response
5459
return p.handleHTTPRedirect(ctx, w, r, rr, "/login")
5560
}
5661

57-
resp := p.ui.GetArgs()
58-
resp.PageTitle = "AWS SSO"
59-
resp.BaseURL(rr.Upstream.BasePath)
60-
61-
if strings.Contains(r.URL.Path, "/apps/sso") && strings.Contains(r.URL.Path, "metadata.xml") {
62-
// TODO(greenpau): add metadata for realm.
62+
// Parse SSO provider name from URL.
63+
req, err := sso.ParseRequestURL(r)
64+
if err != nil {
65+
return p.handleHTTPRenderError(ctx, w, r, rr, err)
6366
}
6467

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-
}
68+
// Check whether the requested SSO provider exists.
69+
provider, err := p.fetchSingleSignOnProvider(req.ProviderName)
70+
if err != nil {
71+
return p.handleHTTPRenderError(ctx, w, r, rr, err)
8372
}
8473

85-
type roleEntry struct {
86-
Name string
87-
AccountID string
74+
roles := fetchSingleSignOnRoles(provider.GetName(), usr)
75+
76+
switch req.Kind {
77+
case sso.MetadataRequest:
78+
return p.handleHTTPAppsSingleSignOnMetadata(ctx, w, r, rr, provider, roles)
79+
case sso.AssumeRoleRequest:
80+
return p.handleHTTPAppsSingleSignOnAssumeRole(ctx, w, r, rr, provider, roles, usr)
81+
case sso.MenuRequest:
82+
return p.handleHTTPAppsSingleSignOnMenu(ctx, w, r, rr, provider, roles, usr)
8883
}
84+
return p.handleHTTPAppsSingleSignOnMenu(ctx, w, r, rr, provider, roles, usr)
85+
}
8986

90-
roles := []*roleEntry{}
91-
for _, entry := range usr.Claims.Roles {
92-
arr := strings.Split(entry, "/")
93-
if len(arr) != 3 {
94-
continue
95-
}
96-
if arr[0] != "aws" {
97-
continue
98-
}
99-
role := &roleEntry{
100-
Name: arr[2],
101-
AccountID: arr[1],
87+
// handleHTTPAppsSingleSignOnMetadata renders metadata.xml content. It is only available to admin users.
88+
func (p *Portal) handleHTTPAppsSingleSignOnMetadata(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request,
89+
provider sso.SingleSignOnProvider, roles []*assumeRoleEntry) error {
90+
// body := []byte("METADATA")
91+
w.Header().Set("Content-Type", "text/html")
92+
w.WriteHeader(http.StatusOK)
93+
// w.Write(body)
94+
w.Write(provider.GetMetadata())
95+
return nil
96+
}
97+
98+
func (p *Portal) handleHTTPAppsSingleSignOnAssumeRole(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request,
99+
provider sso.SingleSignOnProvider, roles []*assumeRoleEntry, usr *user.User) error {
100+
101+
/*
102+
if strings.Contains(r.URL.Path, "/apps/sso/assume") {
103+
accountRole, err := getEndpoint(r.URL.Path, "/apps/sso/assume/")
104+
if err != nil {
105+
p.logger.Warn(
106+
"SSO request failed",
107+
zap.String("session_id", rr.Upstream.SessionID),
108+
zap.String("request_id", rr.ID),
109+
zap.String("error", "malformed SSO request"),
110+
)
111+
} else {
112+
assumedRole = true
113+
arr := strings.SplitN(accountRole, "/", 2)
114+
if len(arr) != 2 {
115+
return p.handleHTTPRenderError(ctx, w, r, rr, fmt.Errorf("Malformed SSO request"))
116+
}
117+
accountID = arr[0]
118+
roleName = arr[1]
119+
}
102120
}
103-
roles = append(roles, role)
121+
*/
104122

105-
if assumedRole {
123+
/*
124+
if assumedRole {
106125
if (role.Name == roleName) && (role.AccountID == accountID) {
107126
authorizedRole = true
108127
p.logger.Debug(
@@ -114,22 +133,39 @@ func (p *Portal) handleHTTPAppsSingleSignOn(ctx context.Context, w http.Response
114133
)
115134
}
116135
}
117-
}
136+
*/
118137

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"))
138+
/*
139+
if assumedRole {
140+
if !authorizedRole {
141+
p.logger.Debug(
142+
"Unauthorized SSO assume role request",
143+
zap.String("session_id", rr.Upstream.SessionID),
144+
zap.String("request_id", rr.ID),
145+
zap.String("role_name", roleName),
146+
zap.String("account_id", accountID),
147+
)
148+
return p.handleHTTPRenderError(ctx, w, r, rr, fmt.Errorf("Unauthorized SSO assume role request"))
149+
}
150+
p.logger.Debug("Redirecting to SAML endpoint")
129151
}
130-
p.logger.Debug("Redirecting to SAML endpoint")
131-
}
132152
153+
*/
154+
155+
body := []byte("ASSUME ROLE")
156+
w.Header().Set("Content-Type", "text/html")
157+
w.WriteHeader(http.StatusOK)
158+
w.Write(body)
159+
return nil
160+
}
161+
162+
// handleHTTPAppsSingleSignOnMenu renders SSO provider role selection page.
163+
func (p *Portal) handleHTTPAppsSingleSignOnMenu(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request,
164+
provider sso.SingleSignOnProvider, roles []*assumeRoleEntry, usr *user.User) error {
165+
166+
resp := p.ui.GetArgs()
167+
resp.PageTitle = "AWS SSO"
168+
resp.BaseURL(rr.Upstream.BasePath)
133169
resp.Data["role_count"] = len(roles)
134170
resp.Data["roles"] = roles
135171

@@ -139,3 +175,36 @@ func (p *Portal) handleHTTPAppsSingleSignOn(ctx context.Context, w http.Response
139175
}
140176
return p.handleHTTPRenderHTML(ctx, w, http.StatusOK, content.Bytes())
141177
}
178+
179+
func (p *Portal) fetchSingleSignOnProvider(providerName string) (sso.SingleSignOnProvider, error) {
180+
for _, provider := range p.ssoProviders {
181+
if provider.GetName() == providerName {
182+
return provider, nil
183+
}
184+
}
185+
return nil, fmt.Errorf("provider name not found")
186+
}
187+
188+
func (p *Portal) parseSingleSignOnProviderName() (string, string, error) {
189+
return "aws", "metadata", nil
190+
}
191+
192+
func fetchSingleSignOnRoles(providerName string, usr *user.User) []*assumeRoleEntry {
193+
roles := []*assumeRoleEntry{}
194+
for _, entry := range usr.Claims.Roles {
195+
arr := strings.Split(entry, "/")
196+
if len(arr) != 3 {
197+
continue
198+
}
199+
if arr[0] != "aws" {
200+
continue
201+
}
202+
role := &assumeRoleEntry{
203+
Name: arr[2],
204+
AccountID: arr[1],
205+
ProviderName: providerName,
206+
}
207+
roles = append(roles, role)
208+
}
209+
return roles
210+
}

pkg/authn/portal.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ package authn
1616

1717
import (
1818
"context"
19+
"sort"
20+
1921
"github.com/greenpau/go-authcrunch/pkg/acl"
2022
"github.com/greenpau/go-authcrunch/pkg/authn/cache"
2123
"github.com/greenpau/go-authcrunch/pkg/authn/cookie"
@@ -31,14 +33,14 @@ import (
3133
"github.com/greenpau/go-authcrunch/pkg/registry"
3234
"github.com/greenpau/go-authcrunch/pkg/sso"
3335
cfgutil "github.com/greenpau/go-authcrunch/pkg/util/cfg"
34-
"sort"
3536

3637
"fmt"
37-
"github.com/google/uuid"
38-
"go.uber.org/zap"
3938
"path"
4039
"strings"
4140
"time"
41+
42+
"github.com/google/uuid"
43+
"go.uber.org/zap"
4244
)
4345

4446
const (

pkg/authn/ui/pages.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -2196,7 +2196,7 @@ function u2f_token_authenticate(formID, btnID) {
21962196
21972197
{{ if gt .Data.role_count 0 }}
21982198
<div class="pb-4 pt-4">
2199-
<p class="app-inp-lbl">Assume the following roles on the associated AWS accounts.</p>
2199+
<p class="app-inp-lbl">Assume any of the following roles on the associated AWS accounts by clicking the name of the role.</p>
22002200
</div>
22012201
22022202
<div class="flex flex-col">
@@ -2213,7 +2213,7 @@ function u2f_token_authenticate(formID, btnID) {
22132213
{{ range .Data.roles }}
22142214
<tr>
22152215
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-primary-700 sm:pl-6 md:pl-0 leading-none">
2216-
<span>{{ brsplitline .Name }}</span>
2216+
<a href="{{ pathjoin $.ActionEndpoint "/apps/sso" .ProviderName "assume" .AccountID .Name }}">{{ brsplitline .Name }}</a>
22172217
</td>
22182218
<td class="whitespace-nowrap py-4 px-3 text-sm text-primary-500">{{ .AccountID }}</td>
22192219
</tr>

pkg/errors/sso.go

+2
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,6 @@ package errors
1818
const (
1919
ErrSingleSignOnProviderConfigInvalid StandardError = "invalid sso provider config: %v: %v"
2020
ErrSingleSignOnProviderConfigureLoggerNotFound StandardError = "sso provider configuration has no logger"
21+
ErrSingleSignOnProviderRequestMalformed StandardError = "malformed sso provider request"
22+
// ErrSingleSignOnProviderRequestInvalid StandardError = "invalid sso provider request: %v"
2123
)

pkg/identity/database.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func init() {
6363
app.Documentation = "https://github.com/greenpau/go-authcrunch"
6464
app.SetVersion(appVersion, "1.0.35")
6565
app.SetGitBranch(gitBranch, "main")
66-
app.SetGitCommit(gitCommit, "v1.0.35")
66+
app.SetGitCommit(gitCommit, "v1.0.35-6-g39f4bc2")
6767
app.SetBuildUser(buildUser, "")
6868
app.SetBuildDate(buildDate, "")
6969
}

pkg/sso/provider.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package sso
1616

1717
import (
1818
"encoding/json"
19+
1920
"github.com/greenpau/go-authcrunch/pkg/errors"
2021
"go.uber.org/zap"
2122
)
@@ -27,11 +28,12 @@ type SingleSignOnProvider interface {
2728
GetConfig() map[string]interface{}
2829
Configure() error
2930
Configured() bool
31+
GetMetadata() []byte
3032
}
3133

3234
// Provider represents sso provider.
3335
type Provider struct {
34-
config *SingleSignOnProviderConfig `json:"config,omitempty" xml:"config,omitempty" yaml:"config,omitempty"`
36+
config *SingleSignOnProviderConfig
3537
configured bool
3638
logger *zap.Logger
3739
}
@@ -86,3 +88,8 @@ func NewSingleSignOnProvider(cfg *SingleSignOnProviderConfig, logger *zap.Logger
8688

8789
return p, nil
8890
}
91+
92+
// GetDriver returns the name of the driver associated with the provider.
93+
func (p *Provider) GetMetadata() []byte {
94+
return []byte("METADATA")
95+
}

0 commit comments

Comments
 (0)