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) => {