diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index 0baf7fee..e22ce748 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -22,8 +22,9 @@ require ( github.com/go-git/go-git/v5 v5.4.2 github.com/gorilla/mux v1.8.0 github.com/h2non/filetype v1.1.3 + github.com/nirasan/go-oauth-pkce-code-verifier v0.0.0-20170819232839-0fbfe93532da // indirect github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.1.86 + github.com/shuffle/shuffle-shared v0.1.94 go4.org v0.0.0-20201209231011-d4a079459e60 // indirect golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce google.golang.org/api v0.65.0 diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 0b9b8c9a..c8b147a5 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -11,6 +11,7 @@ import ( //"crypto/tls" //"crypto/x509" + "encoding/base64" "encoding/hex" "encoding/json" "errors" @@ -18,6 +19,7 @@ import ( "io" "io/ioutil" "log" + "math/rand" "net/http" "net/url" "os" @@ -53,6 +55,8 @@ import ( "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/storage/memory" + //cv "github.com/nirasan/go-oauth-pkce-code-verifier" + //githttp "gopkg.in/src-d/go-git.v4/plumbing/transport/http" // Random @@ -1166,8 +1170,16 @@ func handleContact(resp http.ResponseWriter, request *http.Request) { resp.Write([]byte(fmt.Sprintf(`{"success": true, "message": "Thanks for reaching out. We will contact you soon!"}`))) } +func verifier() (*shuffle.CodeVerifier, error) { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + b := make([]byte, 32, 32) + for i := 0; i < 32; i++ { + b[i] = byte(r.Intn(255)) + } + return shuffle.CreateCodeVerifierFromBytes(b) +} + func checkAdminLogin(resp http.ResponseWriter, request *http.Request) { - log.Printf("In admin login request?") cors := shuffle.HandleCors(resp, request) if cors { return @@ -1190,10 +1202,63 @@ func checkAdminLogin(resp http.ResponseWriter, request *http.Request) { return } - //ssoUrl = org.SSOConfig.SOSOEntrypoint - redirectUri := shuffle.SSOUrl + baseSSOUrl := "" + handled := []string{} + for _, user := range users { + if shuffle.ArrayContains(handled, user.ActiveOrg.Id) { + continue + } + + handled = append(handled, user.ActiveOrg.Id) + org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id) + if err != nil { + log.Printf("[WARNING] Error getting org in admin check: %s", err) + continue + } + + // No childorg setup, only parent org + if len(org.ManagerOrgs) > 0 || len(org.CreatorOrg) > 0 { + continue + } + + // Should run calculations + if len(org.SSOConfig.OpenIdAuthorization) > 0 { + log.Printf("[DEBUG] Found OpenID url (PKCE). Extra redirect check: %s", request.URL.String()) + baseSSOUrl = org.SSOConfig.OpenIdAuthorization + + codeChallenge := uuid.NewV4().String() + //h.Write([]byte(v.Value)) + verifier, verifiererr := verifier() + if verifiererr == nil { + codeChallenge = verifier.Value + } + + //log.Printf("[DEBUG] Got challenge value %s (pre state)", codeChallenge) + + redirectUrl := url.QueryEscape("http://localhost:5001/api/v1/login_openid") + state := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("org=%s&challenge=%s&redirect=%s", org.Id, codeChallenge, redirectUrl))) + + // has to happen after initial value is stored + if verifiererr == nil { + codeChallenge = verifier.CodeChallengeS256() + } + + //log.Printf("[DEBUG] Got challenge value %s (POST state)", codeChallenge) + + baseSSOUrl += fmt.Sprintf("?client_id=%s&response_type=code&scope=openid&redirect_uri=%s&state=%s&code_challenge_method=S256&code_challenge=%s", org.SSOConfig.OpenIdClientId, redirectUrl, state, codeChallenge) + break + } + + if len(org.SSOConfig.SSOEntrypoint) > 0 { + log.Printf("[DEBUG] Found SAML SSO url") + baseSSOUrl = org.SSOConfig.SSOEntrypoint + break + } + } + + log.Printf("[DEBUG] URL: %s", baseSSOUrl) resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "redirect", "sso_url": "%s"}`, redirectUri))) + resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "redirect", "sso_url": "%s"}`, baseSSOUrl))) } func handleLogin(resp http.ResponseWriter, request *http.Request) { @@ -3325,11 +3390,6 @@ func createFs(basepath, pathname string) (billy.Filesystem, error) { return err } - //if strings.Contains(path, "yaml") { - // log.Printf("PATH: %s -> %s", path, fullpath) - // //log.Printf("DATA: %s", string(srcData)) - //} - dst, err := fs.Create(fullpath) if err != nil { log.Printf("Dst error: %s", err) @@ -5882,7 +5942,8 @@ func initHandlers() { // Docker orborus specific - downloads an image r.HandleFunc("/api/v1/get_docker_image", getDockerImage).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/migrate_database", migrateDatabase).Methods("POST", "OPTIONS") - r.HandleFunc("/api/v1/login_sso", shuffle.HandleSSO).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/login_sso", shuffle.HandleSSO).Methods("GET", "POST", "OPTIONS") + r.HandleFunc("/api/v1/login_openid", shuffle.HandleOpenId).Methods("GET", "OPTIONS") // Important for email, IDS etc. Create this by: // PS: For cloud, this has to use cloud storage. diff --git a/frontend/src/components/OrgHeader.jsx b/frontend/src/components/OrgHeader.jsx index 49897638..3d9f0136 100644 --- a/frontend/src/components/OrgHeader.jsx +++ b/frontend/src/components/OrgHeader.jsx @@ -97,6 +97,30 @@ const OrgHeader = (props) => { ? "" : selectedOrganization.defaults.notification_workflow ); + const [openidClientId, setOpenidClientId] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.client_id === undefined || + selectedOrganization.sso_config.client_id.length === 0 + ? "" + : selectedOrganization.sso_config.client_id + ); + const [openidAuthorization, setOpenidAuthorization] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.openid_authorization === undefined || + selectedOrganization.sso_config.openid_authorization.length === 0 + ? "" + : selectedOrganization.sso_config.openid_authorization + ); + const [openidToken, setOpenidToken] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.openid_token=== undefined || + selectedOrganization.sso_config.openid_token.length === 0 + ? "" + : selectedOrganization.sso_config.openidtoken_ + ) const [file, setFile] = React.useState(""); const [fileBase64, setFileBase64] = React.useState( @@ -145,6 +169,7 @@ const OrgHeader = (props) => { defaults, sso_config ) => { + const data = { name: name, description: description, @@ -216,6 +241,9 @@ const OrgHeader = (props) => { { sso_entrypoint: ssoEntrypoint, sso_certificate: ssoCertificate, + client_id: openidClientId, + openid_authorization: openidAuthorization, + openid_token: openidToken, } ) } @@ -548,79 +576,200 @@ const OrgHeader = (props) => { )} - - - SSO Entrypoint (IdP) - 0 - } - id="outlined-with-placeholder" - margin="normal" - variant="outlined" - placeholder="The entrypoint URL from your provider" - value={ssoEntrypoint} - onChange={(e) => { - setSsoEntrypoint(e.target.value); - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - - - - - SSO Certificate (X509) - { - setSsoCertificate(e.target.value); - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - - + {isCloud ? null : + + OpenID connect + + + + Client ID + 0 + } + id="outlined-with-placeholder" + margin="normal" + variant="outlined" + placeholder="The OpenID client ID from the identity provider" + value={openidClientId} + onChange={(e) => { + setOpenidClientId(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + + + Authorization URL + { + setOpenidAuthorization(e.target.value) + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + + + Token URL + { + setOpenidToken(e.target.value) + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + + + } + {isCloud ? null : + + SAML SSO (v1.1) + + + + SSO Entrypoint (IdP) + 0 + } + id="outlined-with-placeholder" + margin="normal" + variant="outlined" + placeholder="The entrypoint URL from your provider" + value={ssoEntrypoint} + onChange={(e) => { + setSsoEntrypoint(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + + + SSO Certificate (X509) + { + setSsoCertificate(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + + + + + } {/* {expanded ? diff --git a/frontend/src/views/LoginPage.jsx b/frontend/src/views/LoginPage.jsx index fd33f22e..19922e43 100644 --- a/frontend/src/views/LoginPage.jsx +++ b/frontend/src/views/LoginPage.jsx @@ -1,5 +1,5 @@ /* eslint-disable react/no-multi-comp */ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { makeStyles } from "@material-ui/styles"; import { useInterval } from "react-powerhooks"; @@ -55,6 +55,7 @@ const LoginDialog = (props) => { const [MFAField, setMFAField] = useState(false); const [MFAValue, setMFAValue] = useState(""); + // Used to swap from login to register. True = login, false = register const classes = useStyles(); @@ -468,6 +469,7 @@ const LoginDialog = (props) => {