Skip to content
Snippets Groups Projects
Commit 4eb280e0 authored by Nicolas Pernoud's avatar Nicolas Pernoud
Browse files

chore: test and fixes

parent 9b2caafe
No related branches found
No related tags found
No related merge requests found
Showing
with 202 additions and 462 deletions
[ ] Remove references to localhost and ports
[ ] Tests
[ ] Tests : matcher, mandatement and missing FC
[ ] OIDC : correct id token and user info
[ ] Remove useless fmt.Print
[ ] Obviously : TODOs
[ ] Css and JS messages
[ ] Business : SIRET handling (work out SIREN for mandatement)
[ ] Business : mandate duration
[ ] Business : allow user to see his mandates and delete them
[ ] Business : allow uncascadables mandates
[ ] Admin view, with Client ID and Client Secret Management
[ ] TODOs scattered around the code
[ ] UI : message animations
package rootmux
import (
"net/http"
"encoding/base64"
"net/http/cookiejar"
"net/http/httptest"
"net/url"
"os"
"regexp"
"strings"
"sync"
"testing"
"forge.grandlyon.com/npernoud/glcpro/pkg/apientreprise"
"forge.grandlyon.com/npernoud/glcpro/pkg/franceconnect"
"forge.grandlyon.com/npernoud/glcpro/pkg/tester"
"forge.grandlyon.com/npernoud/glcpro/pkg/tokens"
)
var (
noH map[string]string
)
func TestMain(m *testing.M) {
// Create the france connect mock server
fcServer := httptest.NewServer(franceconnect.CreateMock())
defer fcServer.Close()
// Setup to use the france connect mock server
os.Setenv("FC_AUTH", fcServer.URL+"/auth")
os.Setenv("FC_TOKEN", fcServer.URL+"/token")
os.Setenv("FC_USER_INFO", fcServer.URL+"/userinfo")
franceconnect.Init()
// Setup the token manager to use debug mode
os.Setenv("DEBUG_MODE", "true")
tokens.Init()
// Setup the api entreprise handler to use debug mode
apientreprise.Init("../../configs/apicache.json")
code := m.Run()
os.Exit(code)
}
func createTester(t *testing.T) (*httptest.Server, tester.DoFn, tester.DoFn) {
// Create the server
mux := CreateRootMux("web")
......@@ -28,41 +52,34 @@ func createTester(t *testing.T) (*httptest.Server, tester.DoFn, tester.DoFn) {
return ts, tester.CreateServerTester(t, port, jar), tester.CreateServerTester(t, port, nil)
}
func TestAll(t *testing.T) {
// Create the tester
// ATTENTION : The tester (do function) Cookie Jar is shared between test groups.
// So it's like each test group is executed at the same time by the same user, with the same browser.
// If needed, create a DoFn in each group.
ts, do, _ := createTester(t)
defer ts.Close()
testGroup1 := createTestGroup1(do)
testGroup2 := createTestGroup2(do)
// Test the use case 1 : "Cas d'usage 1 : Accès à une démarche depuis un portail de service public pour un dirigeant"
func TestUseCase1(t *testing.T) {
// RUN THE TESTS CONCURRENTLY
var wg sync.WaitGroup
functions := []func(wg *sync.WaitGroup){testGroup1, testGroup2}
for _, f := range functions {
wg.Add(1)
go f(&wg)
}
wg.Wait()
clientRedirectURI := "http://client.org"
}
func createTestGroup1(do tester.DoFn) func(wg *sync.WaitGroup) {
// Create the tester
return func(wg *sync.WaitGroup) {
defer wg.Done()
// Try the healtcheck (must pass)
do("GET", "/api/healthcheck", noH, "", http.StatusOK, "OK")
_, do, _ := createTester(t)
// (We arrive from a client with a query, and the front end add the requested SIRENT to the query), we should be redirected to france connect, login, and be back with an authorisation code
r := do("GET", "/api/oidc/auth?scope=openid%20profile&client_id=A_RANDOM_ID&redirect_uri="+clientRedirectURI+"&response_type=code&state=A_RANDOM_STATE&sirent=000000001", noH, "", 302, "")
r = redirectURIFromBody(r)
r = do("GET", r, noH, "", 302, "")
r = redirectURIFromBody(r)
r = do("GET", r, noH, "", 302, "")
r = redirectURIFromBody(r)
if !strings.Contains(r, clientRedirectURI) {
t.Errorf("no redirection to the client")
}
code := regexp.MustCompile(`.*code=(.*)`).FindStringSubmatch(r)[1]
// We now use the code to get the token
tk := do("POST", "/api/oidc/token", noH, "client_id=A_RANDOM_ID&client_secret=A_RANDOM_SECRET&grant_type=authorization_code&code="+code, 200, "")
tk = regexp.MustCompile(`id_token=(.*)&scope.*`).FindStringSubmatch(tk)[1]
token, _ := base64.StdEncoding.DecodeString(tk)
if !strings.Contains(string(token), "Angela Claire Louise") || !strings.Contains(string(token), "THE TEST COMPANY") {
t.Errorf("id token is not complete")
}
}
func createTestGroup2(do tester.DoFn) func(wg *sync.WaitGroup) {
// Create the tester
return func(wg *sync.WaitGroup) {
defer wg.Done()
// Try the healtcheck (must pass)
do("GET", "/api/healthcheck", noH, "", http.StatusOK, "OK")
}
func redirectURIFromBody(body string) string {
return regexp.MustCompile(`<a href="(.*)">Found</a>`).FindStringSubmatch(body)[1]
}
......@@ -61,7 +61,7 @@
Pour valider cette demande, veuillez cliquer sur ce lien :
<a
class="validate-btn"
href="http://localhost:8080/api/matcher/validate?code={{.Token}}"
href="{{.Hostname}}/api/matcher/validate?code={{.Token}}"
>Valider la demande</a
>
</p>
......
......@@ -12,9 +12,8 @@ import (
"forge.grandlyon.com/npernoud/glcpro/pkg/common"
)
const cachePath = "./configs/apicache.json"
var (
cachePath string
apiToken string
debugMode bool
sirenRegexp = regexp.MustCompile(`^\d{9}$`)
......@@ -123,18 +122,20 @@ type Etablissement struct {
} `json:"etat_administratif"`
}
// wrapped init function to be used in tests
func iinit() {
// Init is a wrapped init function to be used in tests
func Init(cp string) {
apiToken = common.StringValueFromEnv("API_TOKEN", "")
debugMode = common.BoolValueFromEnv("DEBUG_MODE", false)
// if in debugMode, load the cache
cachePath = cp
if debugMode {
common.Load(cachePath, &cache)
}
}
func init() {
iinit()
Init("./configs/apicache.json")
}
func GetData(sirent string) (Entreprise, Etablissement, error) {
......
......@@ -9,7 +9,7 @@ import (
func TestGetData(t *testing.T) {
os.Setenv("DEBUG_MODE", "true")
iinit()
Init("../../configs/apicache.json")
type args struct {
sirent string
}
......
{
"000000001": {
"etablissement": {
"siege_social": false,
"siret": "",
"naf": "",
"libelle_naf": "",
"date_mise_a_jour": 0,
"tranche_effectif_salarie_etablissement": {
"de": 0,
"a": 0,
"code": "",
"date_reference": "",
"intitule": ""
},
"date_creation_etablissement": 0,
"region_implantation": {
"code": "",
"value": ""
},
"commune_implantation": {
"code": "",
"value": ""
},
"pays_implantation": {
"code": "",
"value": ""
},
"diffusable_commercialement": false,
"enseigne": "",
"adresse": {
"l1": "",
"l2": "",
"l3": "",
"l4": "",
"l5": "",
"l6": "",
"l7": "",
"numero_voie": "",
"type_voie": "",
"nom_voie": "",
"complement_adresse": "",
"code_postal": "",
"localite": "",
"code_insee_localite": "",
"cedex": ""
},
"etat_administratif": {
"value": "",
"date_fermeture": ""
}
},
"entreprise": {
"siren": "000000001",
"capital_social": 15000,
"numero_tva_intracommunautaire": "----",
"forme_juridique": "Société à responsabilité limitée (sans autre indication)",
"forme_juridique_code": "5499",
"nom_commercial": "",
"procedure_collective": false,
"enseigne": "",
"libelle_naf_entreprise": "Entreprise de test",
"naf_entreprise": "5610C",
"raison_sociale": "THE TEST COMPANY",
"siret_siege_social": "00000000100001",
"code_effectif_entreprise": "02",
"date_creation": 1424041200,
"nom": "",
"prenom": "",
"date_radiation": "",
"categorie_entreprise": "PME",
"tranche_effectif_salarie_entreprise": {
"de": 3,
"a": 5,
"code": "02",
"date_reference": "2018",
"intitule": "3 à 5 salariés"
},
"mandataires_sociaux": [
{
"nom": "Angela Claire Louise",
"prenom": "DUBOIS",
"fonction": "GERANT",
"date_naissance": "1962-08-24",
"date_naissance_timestamp": 863992800,
"dirigeant": true,
"raison_sociale": "",
"identifiant": "",
"type": "PP"
}
],
"etat_administratif": {
"value": "A",
"date_cessation": ""
},
"diffusable_commercialement": true
},
"gateway_error": false
},
"00000000100001": {
"etablissement": {
"siege_social": true,
"siret": "00000000100001",
"naf": "8411Z",
"libelle_naf": "Entreprise de test",
"date_mise_a_jour": 1598343294,
"tranche_effectif_salarie_etablissement": {
"de": 5000,
"a": 9999,
"code": "52",
"date_reference": "2018",
"intitule": "5 000 à 9 999 salariés"
},
"date_creation_etablissement": 1420066800,
"region_implantation": {
"code": "84",
"value": "Auvergne-Rhône-Alpes"
},
"commune_implantation": {
"code": "69383",
"value": "Lyon 3e Arrondissement"
},
"pays_implantation": {
"code": "FR",
"value": "FRANCE"
},
"diffusable_commercialement": true,
"enseigne": "",
"adresse": {
"l1": "THE TEST COMPANY",
"l2": "",
"l3": "",
"l4": "2000 RUE DU LAC",
"l5": "BP 3120",
"l6": "69003 LYON 3EME",
"l7": "FRANCE",
"numero_voie": "2000",
"type_voie": "RUE",
"nom_voie": "DU LAC",
"complement_adresse": "",
"code_postal": "69003",
"localite": "LYON 3EME",
"code_insee_localite": "69383",
"cedex": ""
},
"etat_administratif": {
"value": "A",
"date_fermeture": ""
}
},
"entreprise": {
"siren": "",
"capital_social": 0,
"numero_tva_intracommunautaire": "",
"forme_juridique": "",
"forme_juridique_code": "",
"nom_commercial": "",
"procedure_collective": false,
"enseigne": "",
"libelle_naf_entreprise": "",
"naf_entreprise": "",
"raison_sociale": "",
"siret_siege_social": "",
"code_effectif_entreprise": "",
"date_creation": 0,
"nom": "",
"prenom": "",
"date_radiation": "",
"categorie_entreprise": "",
"tranche_effectif_salarie_entreprise": {
"de": 0,
"a": 0,
"code": "",
"date_reference": "",
"intitule": ""
},
"mandataires_sociaux": null,
"etat_administratif": {
"value": "",
"date_cessation": ""
},
"diffusable_commercialement": false
},
"gateway_error": false
},
"200046977": {
"etablissement": {
"siege_social": false,
"siret": "",
"naf": "",
"libelle_naf": "",
"date_mise_a_jour": 0,
"tranche_effectif_salarie_etablissement": {
"de": 0,
"a": 0,
"code": "",
"date_reference": "",
"intitule": ""
},
"date_creation_etablissement": 0,
"region_implantation": {
"code": "",
"value": ""
},
"commune_implantation": {
"code": "",
"value": ""
},
"pays_implantation": {
"code": "",
"value": ""
},
"diffusable_commercialement": false,
"enseigne": "",
"adresse": {
"l1": "",
"l2": "",
"l3": "",
"l4": "",
"l5": "",
"l6": "",
"l7": "",
"numero_voie": "",
"type_voie": "",
"nom_voie": "",
"complement_adresse": "",
"code_postal": "",
"localite": "",
"code_insee_localite": "",
"cedex": ""
},
"etat_administratif": {
"value": "",
"date_fermeture": ""
}
},
"entreprise": {
"siren": "200046977",
"capital_social": 0,
"numero_tva_intracommunautaire": "FR03200046977",
"forme_juridique": "(Autre) Collectivité territoriale",
"forme_juridique_code": "7229",
"nom_commercial": "",
"procedure_collective": false,
"enseigne": "",
"libelle_naf_entreprise": "Administration publique générale",
"naf_entreprise": "8411Z",
"raison_sociale": "METROPOLE DE LYON",
"siret_siege_social": "20004697700019",
"code_effectif_entreprise": "52",
"date_creation": 1420066800,
"nom": "",
"prenom": "",
"date_radiation": "",
"categorie_entreprise": "GE",
"tranche_effectif_salarie_entreprise": {
"de": 5000,
"a": 9999,
"code": "52",
"date_reference": "2018",
"intitule": "5 000 à 9 999 salariés"
},
"mandataires_sociaux": [],
"etat_administratif": {
"value": "A",
"date_cessation": ""
},
"diffusable_commercialement": true
},
"gateway_error": false
},
"20004697700019": {
"etablissement": {
"siege_social": true,
"siret": "20004697700019",
"naf": "8411Z",
"libelle_naf": "Administration publique générale",
"date_mise_a_jour": 1598343294,
"tranche_effectif_salarie_etablissement": {
"de": 5000,
"a": 9999,
"code": "52",
"date_reference": "2018",
"intitule": "5 000 à 9 999 salariés"
},
"date_creation_etablissement": 1420066800,
"region_implantation": {
"code": "84",
"value": "Auvergne-Rhône-Alpes"
},
"commune_implantation": {
"code": "69383",
"value": "Lyon 3e Arrondissement"
},
"pays_implantation": {
"code": "FR",
"value": "FRANCE"
},
"diffusable_commercialement": true,
"enseigne": "",
"adresse": {
"l1": "METROPOLE DE LYON",
"l2": "",
"l3": "",
"l4": "20 RUE DU LAC",
"l5": "BP 3103",
"l6": "69003 LYON 3EME",
"l7": "FRANCE",
"numero_voie": "20",
"type_voie": "RUE",
"nom_voie": "DU LAC",
"complement_adresse": "",
"code_postal": "69003",
"localite": "LYON 3EME",
"code_insee_localite": "69383",
"cedex": ""
},
"etat_administratif": {
"value": "A",
"date_fermeture": ""
}
},
"entreprise": {
"siren": "",
"capital_social": 0,
"numero_tva_intracommunautaire": "",
"forme_juridique": "",
"forme_juridique_code": "",
"nom_commercial": "",
"procedure_collective": false,
"enseigne": "",
"libelle_naf_entreprise": "",
"naf_entreprise": "",
"raison_sociale": "",
"siret_siege_social": "",
"code_effectif_entreprise": "",
"date_creation": 0,
"nom": "",
"prenom": "",
"date_radiation": "",
"categorie_entreprise": "",
"tranche_effectif_salarie_entreprise": {
"de": 0,
"a": 0,
"code": "",
"date_reference": "",
"intitule": ""
},
"mandataires_sociaux": null,
"etat_administratif": {
"value": "",
"date_cessation": ""
},
"diffusable_commercialement": false
},
"gateway_error": false
}
}
......@@ -6,6 +6,7 @@ import (
"encoding/base64"
"encoding/json"
"io"
"net"
"net/http"
"os"
"path"
......@@ -147,3 +148,12 @@ func BoolValueFromEnv(ev string, def bool) bool {
}
return v
}
// GetDomain remove the port from an host only if present
func GetDomain(r *http.Request) string {
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
return r.Host
}
return host
}
......@@ -9,8 +9,30 @@ import (
"regexp"
"strconv"
"strings"
"forge.grandlyon.com/npernoud/glcpro/pkg/common"
)
var (
fcAuth string
fcToken string
fcUserInfo string
fcClientID string
fcClientSecret string
)
func Init() {
fcAuth = common.StringValueFromEnv("FC_AUTH", "https://fcp.integ01.dev-franceconnect.fr/api/v1/authorize")
fcToken = common.StringValueFromEnv("FC_TOKEN", "https://fcp.integ01.dev-franceconnect.fr/api/v1/token")
fcUserInfo = common.StringValueFromEnv("FC_USER_INFO", "https://fcp.integ01.dev-franceconnect.fr/api/v1/userinfo?schema=openid")
fcClientID = common.StringValueFromEnv("FC_CLIENT_ID", "211286433e39cce01db448d80181bdfd005554b19cd51b3fe7943f6b3b86ab6e")
fcClientSecret = common.StringValueFromEnv("FC_CLIENT_SECRET", "2791a731e6a59f56b6b4dd0d08c9b1f593b5f3658b9fd731cb24248e2669af4b")
}
func init() {
Init()
}
type Identity struct {
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
......@@ -23,33 +45,24 @@ type Identity struct {
}
func RedirectToFranceConnect(w http.ResponseWriter, r *http.Request) {
// TODO : France Connect parameters as env variables
fcAuth := "https://fcp.integ01.dev-franceconnect.fr/api/v1/authorize"
fcClientID := "211286433e39cce01db448d80181bdfd005554b19cd51b3fe7943f6b3b86ab6e"
params := url.Values{}
// TODO : State and nonce
params.Add("state", "A_RANDOM_STATE")
params.Add("nonce", "A_RANDOM_NONCE")
params.Add("scope", "openid identite_pivot")
// TODO : Redirect from request
params.Add("redirect_uri", "http://localhost:8080"+"/callback")
params.Add("redirect_uri", getRedirectUri(r)+"/callback")
params.Add("client_id", fcClientID)
params.Add("response_type", "code")
http.Redirect(w, r, fcAuth+"?"+params.Encode(), http.StatusFound)
}
func HandleCallBack(w http.ResponseWriter, r *http.Request) (Identity, error) {
// TODO : fcToken and client secret from env
fcToken := "https://fcp.integ01.dev-franceconnect.fr/api/v1/token"
fcUserInfo := "https://fcp.integ01.dev-franceconnect.fr/api/v1/userinfo?schema=openid"
fcClientID := "211286433e39cce01db448d80181bdfd005554b19cd51b3fe7943f6b3b86ab6e"
fcClientSecret := "2791a731e6a59f56b6b4dd0d08c9b1f593b5f3658b9fd731cb24248e2669af4b"
// TODO : Check the state
fcCode := r.URL.Query().Get("code")
data := url.Values{}
data.Set("grant_type", "authorization_code")
data.Set("redirect_uri", "http://localhost:8080/callback")
data.Set("redirect_uri", getRedirectUri(r)+"/callback")
data.Set("client_id", fcClientID)
data.Set("client_secret", fcClientSecret)
data.Set("code", fcCode)
......@@ -68,7 +81,6 @@ func HandleCallBack(w http.ResponseWriter, r *http.Request) (Identity, error) {
}
// Get the access_token information
rBody, _ := ioutil.ReadAll(resp.Body)
fmt.Printf("rBody: %v\n", string(rBody))
accessToken := regexp.MustCompile(`.*"access_token":"([^"]*)`).FindStringSubmatch(string(rBody))[1]
// Call the user info endpoint
......@@ -89,3 +101,11 @@ func HandleCallBack(w http.ResponseWriter, r *http.Request) (Identity, error) {
}
return i, nil
}
func getRedirectUri(r *http.Request) string {
if r.TLS != nil {
return "https://" + r.Host
} else {
return "http://" + r.Host
}
}
package franceconnect
import (
"fmt"
"net/http"
"strings"
)
// CreateMockOAuth2 creates a mock OAuth2 serve mux for development purposes
func CreateMock() *http.ServeMux {
mux := http.NewServeMux()
// Returns authorization code back to the user
mux.HandleFunc("/auth", func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
redir := strings.Replace(query.Get("redirect_uri"), "/callback", "/api/oidc/callback", 1) + "?state=" + query.Get("state") + "&code=mock_code"
http.Redirect(w, r, redir, http.StatusFound)
})
// Returns access token back to the user
mux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"access_token":"988d0dc4-682d-4edd-a6dc-cf6dec915270","token_type":"Bearer","expires_in":60,"id_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL2ZjcC5pbnRlZzAxLmRldi1mcmFuY2Vjb25uZWN0LmZyIiwic3ViIjoiYjYwNDhlOTViYjEzNGVjNWIxZDFlMWZhNjlmMjg3MTcyZTkxNzIyYjkzNTRkNjM3YTFiY2YyZWJiMGZkMmVmNXYxIiwiYXVkIjoiMjExMjg2NDMzZTM5Y2NlMDFkYjQ0OGQ4MDE4MWJkZmQwMDU1NTRiMTljZDUxYjNmZTc5NDNmNmIzYjg2YWI2ZSIsImV4cCI6MTYyNDYxMDk4MywiaWF0IjoxNjI0NjEwOTIzLCJub25jZSI6IkFfUkFORE9NX05PTkNFIiwiaWRwIjoiRkMiLCJhY3IiOiJlaWRhczMiLCJhbXIiOm51bGx9.zxfAVxZh9Jy-HnFFvszQf-vu_8PBjXu9oi1oD5E-94c"}`))
})
// Returns userinfo back to the user
mux.HandleFunc("/userinfo", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"given_name":"Angela Claire Louise","family_name":"DUBOIS","birthdate":"1962-08-24","gender":"female","birthplace":"75107","birthcountry":"99100","preferred_username":"","sub":"b6048e95bb134ec5b1d1e1fa69f287172e91722b9354d637a1bcf2ebb0fd2ef5v1"}`))
})
// Logout
mux.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Logout OK")
})
return mux
}
......@@ -61,7 +61,7 @@ func CreateMatcherServer() *http.ServeMux {
}
// Send a mail to the CEO with the aforementionned token
err = sendMailToMandater(string(email), rd.Id.GivenName+" "+rd.Id.FamilyName, rd.Sirent, token)
err = sendMailToMandater(string(email), rd.Id.GivenName+" "+rd.Id.FamilyName, rd.Sirent, token, r.Host)
if err != nil {
http.Error(w, "could not send email", http.StatusBadRequest)
return
......@@ -76,10 +76,10 @@ func CreateMatcherServer() *http.ServeMux {
tokens.ExtractAndValidateToken(r, "", &md, false)
// Store the demand in a cookie
tokens.CreateCookie(md, "localhost", "MandateDemand", 60*time.Second, w)
tokens.CreateCookie(md, r.Host, "MandateDemand", 60*time.Second, w)
// Set a cookie to redirect to the needed callback route from the front end generic callback
cookie := http.Cookie{Name: "callbackRoute", Domain: "localhost", Path: "/", Value: "/api/matcher/callback", MaxAge: 60, Secure: false, HttpOnly: false, SameSite: http.SameSiteLaxMode}
cookie := http.Cookie{Name: "callbackRoute", Domain: r.Host, Path: "/", Value: "/api/matcher/callback", MaxAge: 60, Secure: false, HttpOnly: false, SameSite: http.SameSiteLaxMode}
http.SetCookie(w, &cookie)
// Perform an France Connect authentication ... and wait for the callback
......@@ -138,7 +138,7 @@ func isEmailValid(e string) bool {
return emailRegex.MatchString(e)
}
func sendMailToMandater(email string, mandate string, sirent string, token string) error {
func sendMailToMandater(email string, mandate string, sirent string, token string, hostname string) error {
// Receiver email address.
to := []string{
email,
......@@ -155,13 +155,15 @@ func sendMailToMandater(email string, mandate string, sirent string, token strin
body.Write([]byte(fmt.Sprintf("Subject: GLC PRO - Demande de validation de mandatement \n%s\n\n", mimeHeaders)))
t.Execute(&body, struct {
Mandate string
Sirent string
Token string
Mandate string
Sirent string
Token string
Hostname string
}{
Mandate: mandate,
Sirent: sirent,
Token: token,
Mandate: mandate,
Sirent: sirent,
Token: token,
Hostname: hostname,
})
// Sending email.
......
......@@ -19,7 +19,7 @@ func Test_sendMailToMandater(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := sendMailToMandater(tt.args.email, tt.args.mandate, tt.args.sirent, tt.args.token); (err != nil) != tt.wantErr {
if err := sendMailToMandater(tt.args.email, tt.args.mandate, tt.args.sirent, tt.args.token, "http://localhost"); (err != nil) != tt.wantErr {
t.Errorf("sendMailToMandater() error = %v, wantErr %v", err, tt.wantErr)
}
})
......
......@@ -3,6 +3,7 @@ package oidcserver
import (
"encoding/base64"
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
"strings"
......@@ -64,10 +65,10 @@ func CreateOIDCServer() *http.ServeMux {
// Store the client data in a cookie
rd.State = query.Get("state")
rd.Sirent = query.Get("sirent")
tokens.CreateCookie(rd, "localhost", "ClientRequestData", 60*time.Second, w)
tokens.CreateCookie(rd, common.GetDomain(r), "ClientRequestData", 60*time.Second, w)
// Set a cookie to redirect to the needed callback route from the front end generic callback
cookie := http.Cookie{Name: "callbackRoute", Domain: "localhost", Path: "/", Value: "/api/oidc/callback", MaxAge: 60, Secure: false, HttpOnly: false, SameSite: http.SameSiteLaxMode}
cookie := http.Cookie{Name: "callbackRoute", Domain: common.GetDomain(r), Path: "/", Value: "/api/oidc/callback", MaxAge: 60, Secure: false, HttpOnly: false, SameSite: http.SameSiteLaxMode}
http.SetCookie(w, &cookie)
// Redirect to France Connect with the callback as parameter
......@@ -109,7 +110,7 @@ func CreateOIDCServer() *http.ServeMux {
if e.MandatairesSociaux[0].Nom != i.FamilyName || e.MandatairesSociaux[0].Prenom != i.GivenName || e.MandatairesSociaux[0].DateNaissance != i.Birthdate { // If not redirect to the matcher to ask for a mandate
// Add the asker identity to the request data
rd.Id = i
tokens.CreateCookie(rd, "localhost", "ClientRequestData", 60*time.Second, w)
tokens.CreateCookie(rd, common.GetDomain(r), "ClientRequestData", 60*time.Second, w)
http.Redirect(w, r, "/matcher", http.StatusFound)
return
}
......@@ -129,9 +130,13 @@ func CreateOIDCServer() *http.ServeMux {
})
// Returns access token back to the user
// TODO : check if GET request is OK in OIDC protocol or replace with POST
mux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
bdy, _ := ioutil.ReadAll(r.Body)
query, err := url.ParseQuery(string(bdy))
if err != nil {
http.Error(w, "request body is malformed", http.StatusBadRequest)
return
}
/// TODO : check that the client id is an actual client id
// Check that an client id is present
clientID := query.Get("client_id")
......@@ -154,7 +159,7 @@ func CreateOIDCServer() *http.ServeMux {
}
// Get and decode the autorisation code
var id GLCId
_, err := tokens.ExtractAndValidateToken(r, "", &id, false)
err = tokens.UnstoreData(query.Get("code"), &id)
if err != nil {
http.Error(w, "authorization code is invalid: "+err.Error(), http.StatusBadRequest)
return
......
......@@ -2,7 +2,6 @@ package oidcserver
import (
"net/http"
"regexp"
"testing"
"forge.grandlyon.com/npernoud/glcpro/pkg/tester"
......@@ -31,25 +30,23 @@ func TestOIDCServer(t *testing.T) {
// Test that an invalid response_type gives an error
do("GET", "/auth?scope=openid profile&client_id=A_RANDOM_ID&redirect_uri=http://www.grandlyon.com", noH, "", http.StatusNotImplemented, "only authorisation code flow")
do("GET", "/auth?scope=openid profile&client_id=A_RANDOM_ID&redirect_uri=http://www.grandlyon.com&response_type=other", noH, "", http.StatusNotImplemented, "only authorisation code flow")
// Test that a correct request gives a redirection (and store the code for later)
bodyWithCode := do("GET", "/auth?scope=openid profile&client_id=A_RANDOM_ID&redirect_uri=http://www.grandlyon.com&response_type=code&state=A_RANDOM_STATE", noH, "", http.StatusFound, "<a href=\"http://www.grandlyon.com")
code := regexp.MustCompile(`.*code=(.*)"`).FindStringSubmatch(bodyWithCode)[1]
// Test that a correct request gives a redirection
do("GET", "/auth?scope=openid profile&client_id=A_RANDOM_ID&redirect_uri=http://www.grandlyon.com&response_type=code&state=A_RANDOM_STATE", noH, "", http.StatusFound, "")
/////////////////
// TOKEN TESTS //
/////////////////
// Test that no client id gives an error
do("GET", "/token", noH, "", http.StatusBadRequest, "a client id must be present")
do("POST", "/token", noH, "", http.StatusBadRequest, "a client id must be present")
// Test that no client secret gives an error
do("GET", "/token?client_id=A_RANDOM_ID", noH, "", http.StatusBadRequest, "a client secret must be present")
do("POST", "/token", noH, "client_id=A_RANDOM_ID", http.StatusBadRequest, "a client secret must be present")
// Test that an invalid response_type gives an error
do("GET", "/token?client_id=A_RANDOM_ID&client_secret=A_RANDOM_SECRET", noH, "", http.StatusNotImplemented, "only authorisation code flow")
do("GET", "/token?client_id=A_RANDOM_ID&client_secret=A_RANDOM_SECRET&grant_type=other", noH, "", http.StatusNotImplemented, "only authorisation code flow")
do("POST", "/token", noH, "client_id=A_RANDOM_ID&client_secret=A_RANDOM_SECRET", http.StatusNotImplemented, "only authorisation code flow")
do("POST", "/token", noH, "client_id=A_RANDOM_ID&client_secret=A_RANDOM_SECRET&grant_type=other", http.StatusNotImplemented, "only authorisation code flow")
// Test that no code gives an error
do("GET", "/token?client_id=A_RANDOM_ID&client_secret=A_RANDOM_SECRET&grant_type=authorization_code", noH, "", http.StatusBadRequest, "authorization code is invalid")
do("POST", "/token", noH, "client_id=A_RANDOM_ID&client_secret=A_RANDOM_SECRET&grant_type=authorization_code", http.StatusBadRequest, "authorization code is invalid")
// Test that a wrong code gives an error
do("GET", "/token?client_id=A_RANDOM_ID&client_secret=A_RANDOM_SECRET&grant_type=authorization_code&code=NOT_A_CODE", noH, "", http.StatusBadRequest, "authorization code is invalid")
// Test that a correct code give the wanted response
do("GET", "/token?client_id=A_RANDOM_ID&client_secret=A_RANDOM_SECRET&grant_type=authorization_code&code="+code, noH, "", http.StatusOK, "id_token=some very personnal data")
do("POST", "/token", noH, "client_id=A_RANDOM_ID&client_secret=A_RANDOM_SECRET&grant_type=authorization_code&code=NOT_A_CODE", http.StatusBadRequest, "authorization code is invalid")
// Test that a correct code give the wanted response is made in global rootmux test
}
......@@ -42,7 +42,9 @@ func CreateHandlerTester(t *testing.T, h http.Handler) DoFn {
// DoRequestOnServer does a request on listening server
func DoRequestOnServer(t *testing.T, port string, jar *cookiejar.Jar, method string, testURL string, headers map[string]string, payload string, expectedStatus int, expectedBody string) string {
if strings.HasPrefix(testURL, "/") {
if strings.HasPrefix(testURL, "http") {
// Keep as it is
} else if strings.HasPrefix(testURL, "/") {
testURL = "http://localhost:" + port + testURL
} else {
u, _ := url.Parse("http://" + testURL)
......@@ -55,11 +57,13 @@ func DoRequestOnServer(t *testing.T, port string, jar *cookiejar.Jar, method str
for i, v := range headers {
req.Header.Set(i, v)
}
var client *http.Client
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
if jar != nil {
client = &http.Client{Jar: jar}
} else {
client = &http.Client{}
client.Jar = jar
}
res, err := client.Do(req)
if err != nil {
......
......@@ -33,10 +33,14 @@ type manager struct {
}
// Init inits the main token manager
func init() {
func Init() {
m = newManager()
}
func init() {
Init()
}
// newManager creates a manager
func newManager() manager {
var keyConfig struct {
......@@ -54,7 +58,8 @@ func newManager() manager {
}
log.Logger.Printf("Token signing key set : %v\n", string(keyConfig.Key))
return manager{
key: keyConfig.Key,
key: keyConfig.Key,
debugMode: common.BoolValueFromEnv("DEBUG_MODE", false),
}
}
......@@ -130,13 +135,13 @@ func ExtractAndValidateToken(r *http.Request, cookieName string, v interface{},
}(r, checkXSRF)
if err == nil {
return checkXSRF, unstoreData(becsToken, v)
return checkXSRF, UnstoreData(becsToken, v)
}
return false, err
}
// unstoreData decrypt, uncompress, unserialize the token, and returns the data n the value pointed to by v
func unstoreData(becsToken string, v interface{}) error {
// UnstoreData decrypt, uncompress, unserialize the token, and returns the data n the value pointed to by v
func UnstoreData(becsToken string, v interface{}) error {
// Decrypt the token
ecsToken, err := base64.StdEncoding.DecodeString(becsToken)
if err != nil {
......
......@@ -50,7 +50,7 @@ func TestManagerCreateTokenUnStoreData(t *testing.T) {
token, _ := CreateToken(tt.args.data, tt.args.expiration)
m.key = tt.fields.decryptKey
v := user{}
err := unstoreData(token, &v)
err := UnstoreData(token, &v)
got := tt.args.data == v
if (err != nil) != tt.wantErr {
t.Errorf("manager.(un)storeData() error = %v, wantErr %v", err, tt.wantErr)
......
......@@ -31,14 +31,11 @@ class Matcher {
.getElementById(`matcher-do`)
.addEventListener("click", async () => {
try {
const response = await fetch(
`http://localhost:8080/api/matcher/demand`,
{
method: "POST",
credentials: "include",
body: document.getElementById(`matcher-email`).value,
}
);
const response = await fetch(`/api/matcher/demand`, {
method: "POST",
credentials: "include",
body: document.getElementById(`matcher-email`).value,
});
if (response.status !== 200) {
throw new Error(`Could not send mail (status ${response.status})`);
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment