github.com/greenpau/go-authcrunch@v1.1.4/pkg/authn/handle_http_apps_sso.go (about)

     1  // Copyright 2022 Paul Greenberg greenpau@outlook.com
     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 authn
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"net/http"
    21  	"strings"
    22  
    23  	"github.com/greenpau/go-authcrunch/pkg/requests"
    24  	"github.com/greenpau/go-authcrunch/pkg/sso"
    25  	"github.com/greenpau/go-authcrunch/pkg/user"
    26  	"go.uber.org/zap"
    27  )
    28  
    29  type assumeRoleEntry struct {
    30  	Name         string
    31  	AccountID    string
    32  	ProviderName string
    33  }
    34  
    35  func (p *Portal) handleHTTPAppsSingleSignOn(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request, parsedUser *user.User) error {
    36  	p.disableClientCache(w)
    37  	p.injectRedirectURL(ctx, w, r, rr)
    38  
    39  	if parsedUser == nil {
    40  		if rr.Response.RedirectURL == "" {
    41  			return p.handleHTTPRedirect(ctx, w, r, rr, "/login?redirect_url="+r.RequestURI)
    42  		}
    43  		return p.handleHTTPRedirect(ctx, w, r, rr, "/login")
    44  	}
    45  
    46  	usr, err := p.sessions.Get(parsedUser.Claims.ID)
    47  	if err != nil {
    48  		p.deleteAuthCookies(w, r)
    49  		p.logger.Debug(
    50  			"User session not found, redirect to login",
    51  			zap.String("session_id", rr.Upstream.SessionID),
    52  			zap.String("request_id", rr.ID),
    53  			zap.Any("user", parsedUser.Claims),
    54  			zap.Error(err),
    55  		)
    56  		if rr.Response.RedirectURL == "" {
    57  			return p.handleHTTPRedirect(ctx, w, r, rr, "/login?redirect_url="+r.RequestURI)
    58  		}
    59  		return p.handleHTTPRedirect(ctx, w, r, rr, "/login")
    60  	}
    61  
    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)
    66  	}
    67  
    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)
    72  	}
    73  
    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)
    83  	}
    84  	return p.handleHTTPAppsSingleSignOnMenu(ctx, w, r, rr, provider, roles, usr)
    85  }
    86  
    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  	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")
    95  	w.WriteHeader(http.StatusOK)
    96  	w.Write(metadata)
    97  	return nil
    98  }
    99  
   100  func (p *Portal) handleHTTPAppsSingleSignOnAssumeRole(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request,
   101  	provider sso.SingleSignOnProvider, roles []*assumeRoleEntry, usr *user.User) error {
   102  
   103  	/*
   104  		if strings.Contains(r.URL.Path, "/apps/sso/assume") {
   105  			accountRole, err := getEndpoint(r.URL.Path, "/apps/sso/assume/")
   106  			if err != nil {
   107  				p.logger.Warn(
   108  					"SSO request failed",
   109  					zap.String("session_id", rr.Upstream.SessionID),
   110  					zap.String("request_id", rr.ID),
   111  					zap.String("error", "malformed SSO request"),
   112  				)
   113  			} else {
   114  				assumedRole = true
   115  				arr := strings.SplitN(accountRole, "/", 2)
   116  				if len(arr) != 2 {
   117  					return p.handleHTTPRenderError(ctx, w, r, rr, fmt.Errorf("Malformed SSO request"))
   118  				}
   119  				accountID = arr[0]
   120  				roleName = arr[1]
   121  			}
   122  		}
   123  	*/
   124  
   125  	/*
   126  			if assumedRole {
   127  			if (role.Name == roleName) && (role.AccountID == accountID) {
   128  				authorizedRole = true
   129  				p.logger.Debug(
   130  					"SSO assume role request received",
   131  					zap.String("session_id", rr.Upstream.SessionID),
   132  					zap.String("request_id", rr.ID),
   133  					zap.String("role_name", roleName),
   134  					zap.String("account_id", accountID),
   135  				)
   136  			}
   137  		}
   138  	*/
   139  
   140  	/*
   141  			if assumedRole {
   142  			if !authorizedRole {
   143  				p.logger.Debug(
   144  					"Unauthorized SSO assume role request",
   145  					zap.String("session_id", rr.Upstream.SessionID),
   146  					zap.String("request_id", rr.ID),
   147  					zap.String("role_name", roleName),
   148  					zap.String("account_id", accountID),
   149  				)
   150  				return p.handleHTTPRenderError(ctx, w, r, rr, fmt.Errorf("Unauthorized SSO assume role request"))
   151  			}
   152  			p.logger.Debug("Redirecting to SAML endpoint")
   153  		}
   154  
   155  	*/
   156  
   157  	body := []byte("ASSUME ROLE")
   158  	w.Header().Set("Content-Type", "text/html")
   159  	w.WriteHeader(http.StatusOK)
   160  	w.Write(body)
   161  	return nil
   162  }
   163  
   164  // handleHTTPAppsSingleSignOnMenu renders SSO provider role selection page.
   165  func (p *Portal) handleHTTPAppsSingleSignOnMenu(ctx context.Context, w http.ResponseWriter, r *http.Request, rr *requests.Request,
   166  	provider sso.SingleSignOnProvider, roles []*assumeRoleEntry, usr *user.User) error {
   167  
   168  	resp := p.ui.GetArgs()
   169  	resp.PageTitle = "AWS SSO"
   170  	resp.BaseURL(rr.Upstream.BasePath)
   171  	resp.Data["role_count"] = len(roles)
   172  	resp.Data["roles"] = roles
   173  
   174  	content, err := p.ui.Render("apps_sso", resp)
   175  	if err != nil {
   176  		return p.handleHTTPRenderError(ctx, w, r, rr, err)
   177  	}
   178  	return p.handleHTTPRenderHTML(ctx, w, http.StatusOK, content.Bytes())
   179  }
   180  
   181  func (p *Portal) fetchSingleSignOnProvider(providerName string) (sso.SingleSignOnProvider, error) {
   182  	for _, provider := range p.ssoProviders {
   183  		if provider.GetName() == providerName {
   184  			return provider, nil
   185  		}
   186  	}
   187  	return nil, fmt.Errorf("provider name not found")
   188  }
   189  
   190  func (p *Portal) parseSingleSignOnProviderName() (string, string, error) {
   191  	return "aws", "metadata", nil
   192  }
   193  
   194  func fetchSingleSignOnRoles(providerName string, usr *user.User) []*assumeRoleEntry {
   195  	roles := []*assumeRoleEntry{}
   196  	for _, entry := range usr.Claims.Roles {
   197  		arr := strings.Split(entry, "/")
   198  		if len(arr) != 3 {
   199  			continue
   200  		}
   201  		if arr[0] != "aws" {
   202  			continue
   203  		}
   204  		role := &assumeRoleEntry{
   205  			Name:         arr[2],
   206  			AccountID:    arr[1],
   207  			ProviderName: providerName,
   208  		}
   209  		roles = append(roles, role)
   210  	}
   211  	return roles
   212  }