Repository: neicnordic/sensitive-data-archive
Pull request #1460: fix blank page on failed ega login
Author: login: MalinAhlberg
Created: 2025-03-05T07:40:13Z | Merged: 2025-03-18T07:51:53Z

## Related issue(s) and PR(s)
This PR closes #1416 .


## Description
This PR
- fixes the bug that caused the blank page when logging in with bad credentials from the user. _Provided credentials are not valid_ is shown
- fixes the bug that caused the blank page when auth service is badly configured (`auth_cega_id` or `auth_cega_secret` is wrong). _Problems connecting to EGA authenticaton server_ is shown
- fixes the bug that lead to _This page isn't worknig_ when `cega-nss` is down. _EGA authentication server could not be contacted_ is shown.
- fixes some unrelated typos in the README

## How to test
- [ ] `make sda-s3-up`
- [ ] navigate to `localhost:8888`, choose the EGA login, and try to log in with a bad username/password
- [ ] verify that the page is not blank, but contains an error message and a new login form
- [ ] Stop `cega-nss` and verify that the page does not break when trying to log in
- [ ] Change `AUTH_CEGA_SECRET` in `.github/integration/sda-s3-integration.yml` and verify that the page does not turn blank or break when trying to log in.
- [ ] Please test anything else you might come up with

**Additional notes**
No tests are added, manual testing of the webpage will still be the standard.

Linked issue #1416: [Auth] EGA login doesn't work when an error occurs
## Describe the bug

Authenticating with the EGA option results in a blank page.

## Steps to reproduce

make build-all
make sda-s3-up

Authenticate with wrong user/password and the response is a blank page. This should at the least show a page with an authentication error i.e. `incorrect username or password supplied`

## Expected behavior
- [ ] Properly report to the user that something went wrong
  - [ ] wrong username and/or password (401)
  - [ ] badly configured auth service i.e responses of 404, 503, 502 and so on from the backend
- [ ] Tests verifying the fix are added

## Additional context

## Estimation of size
small/medium/big

## Estimation of priority
low/medium/high


Diff:
diff --git a/README.md b/README.md
index d8fd226cf..9a75c7f33 100644
--- a/README.md
+++ b/README.md
@@ -252,9 +252,9 @@ Bootstrap the dependencies with the command: `make k3d-deploy-dependencies`.
 
 Start by building and importing the required containers using the `make k3d-import-images`.
 
-The Postgres and RabbitMQ Needs to be deployed first using the following commands: `make k3d-deplploy-postgres` and `make k3d-deploy-rabbitmq`.
+The Postgres and RabbitMQ Needs to be deployed first using the following commands: `make k3d-deploy-postgres` and `make k3d-deploy-rabbitmq`.
 
-Once the DB and MQ are installed the SDA stack can be installed, here the desired storage backend needs to specified as well (`posix` or `s3`), `make k3d-deplpoy-sda-posix` or `make k3d-deplpoy-sda-s3`.
+Once the DB and MQ are installed the SDA stack can be installed, here the desired storage backend needs to specified as well (`posix` or `s3`), `make k3d-deploy-sda-posix` or `make k3d-deploy-sda-s3`.
 
 #### Testing with ingress
 
diff --git a/sda/cmd/auth/cega.go b/sda/cmd/auth/cega.go
index 54f2919bf..8f99aa997 100644
--- a/sda/cmd/auth/cega.go
+++ b/sda/cmd/auth/cega.go
@@ -7,15 +7,9 @@ import (
 	"strings"
 
 	"github.com/neicnordic/sensitive-data-archive/internal/config"
-	log "github.com/sirupsen/logrus"
 	bcrypt "golang.org/x/crypto/bcrypt"
 )
 
-// EGALoginError is used to store message errors
-type EGALoginError struct {
-	Reason string
-}
-
 // CegaUserResponse captures the response list
 type CegaUserResponse struct {
 	PasswordHash string `json:"passwordHash"`
@@ -49,7 +43,7 @@ func authenticateWithCEGA(conf config.CegaConfig, username string) (*http.Respon
 	req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s", strings.TrimSuffix(conf.AuthURL, "/"), username), payload)
 
 	if err != nil {
-		log.Fatal(err)
+		return nil, err
 	}
 
 	req.Header.Add("Authorization", "Basic "+getb64Credentials(conf.ID, conf.Secret))
diff --git a/sda/cmd/auth/main.go b/sda/cmd/auth/main.go
index 1fe808846..3eac088b6 100644
--- a/sda/cmd/auth/main.go
+++ b/sda/cmd/auth/main.go
@@ -119,24 +119,23 @@ func (auth AuthHandler) postEGA(ctx iris.Context) {
 	res, err := authenticateWithCEGA(auth.Config.Cega, username)
 
 	if err != nil {
-		log.Error(err)
+		log.Errorf("No response from cega, error: %v", err)
+		res = &http.Response{
+			Body:       io.NopCloser(nil),
+			StatusCode: http.StatusInternalServerError,
+		}
 	}
-
 	defer res.Body.Close()
 
 	switch res.StatusCode {
 	case 200:
-		if err != nil {
-			log.Error(err)
-
-			return
-		}
-
 		var ur CegaUserResponse
-		err = json.NewDecoder(res.Body).Decode(&ur)
+		err := json.NewDecoder(res.Body).Decode(&ur)
 
 		if err != nil {
-			log.Error("Failed to parse response: ", err)
+			log.Error("Failed to parse cega response: ", err)
+			s.SetFlash("message", "Problems connecting to EGA authentication server")
+			ctx.Redirect("/ega/login", iris.StatusSeeOther)
 
 			return
 		}
@@ -156,21 +155,31 @@ func (auth AuthHandler) postEGA(ctx iris.Context) {
 			token, expDate, err := generateJwtToken(claims, auth.Config.JwtPrivateKey, auth.Config.JwtSignatureAlg)
 			if err != nil {
 				log.Errorf("error when generating token: %v", err)
+				s.SetFlash("message", "Unexpected error, please try again.")
+				ctx.Redirect("/ega/login", iris.StatusSeeOther)
+
+				return
 			}
 
 			s3conf := getS3ConfigMap(token, auth.Config.S3Inbox, username)
 			s.SetFlash("ega", s3conf)
+
 			ctx.ViewData("infoUrl", auth.Config.InfoURL)
 			ctx.ViewData("infoText", auth.Config.InfoText)
 			ctx.ViewData("User", username)
 			ctx.ViewData("Token", token)
 			ctx.ViewData("ExpDate", expDate)
-
 			err = ctx.View("ega.html")
-			if err != nil {
-				log.Error("Failed to parse response: ", err)
 
-				return
+			if err != nil {
+				log.Error("Failed to create view: ", err)
+
+				// Since the context has already started writing the response to
+				// the client, the resulting page will be ugly, but at least
+				// show an error message, and an oppertunity to log in again.
+				ctx.ViewData("Reason", "Unexpected error, please try again.")
+				err = ctx.View("loginform.html")
+				log.Error("Failed to create backup view: ", err)
 			}
 
 		} else {
@@ -179,11 +188,15 @@ func (auth AuthHandler) postEGA(ctx iris.Context) {
 			ctx.Redirect("/ega/login", iris.StatusSeeOther)
 		}
 
-	case 404:
+	case 500, 502, 503:
 		log.WithFields(log.Fields{"authType": "cega", "user": username}).Error("Failed to authenticate user")
 		s.SetFlash("message", "EGA authentication server could not be contacted")
 		ctx.Redirect("/ega/login", iris.StatusSeeOther)
 
+	case 401:
+		log.WithFields(log.Fields{"authType": "cega", "user": username}).Error("Failed to authenticate service (auth_cega_id/secret)")
+		s.SetFlash("message", "Problems connecting to EGA authentication server")
+		ctx.Redirect("/ega/login", iris.StatusSeeOther)
 	default:
 		log.WithFields(log.Fields{"authType": "cega", "user": username}).Error("Failed to authenticate user")
 		s.SetFlash("message", "Provided credentials are not valid")
@@ -195,26 +208,15 @@ func (auth AuthHandler) postEGA(ctx iris.Context) {
 func (auth AuthHandler) getEGALogin(ctx iris.Context) {
 
 	s := sessions.Get(ctx)
-	message := s.GetFlashString("message")
-	if message == "" {
-		ctx.ViewData("infoUrl", auth.Config.InfoURL)
-		ctx.ViewData("infoText", auth.Config.InfoText)
-		err := ctx.View("loginform.html")
-		if err != nil {
-			log.Error("Failed to return to login form: ", err)
-
-			return
-		}
-
-		return
-	}
 	ctx.ViewData("infoUrl", auth.Config.InfoURL)
 	ctx.ViewData("infoText", auth.Config.InfoText)
-	err := ctx.View("loginform.html", EGALoginError{Reason: message})
+	message := s.GetFlashString("message")
+	if message != "" {
+		ctx.ViewData("Reason", message)
+	}
+	err := ctx.View("loginform.html")
 	if err != nil {
 		log.Error("Failed to view invalid credentials form: ", err)
-
-		return
 	}
 }
 


Reviews:
user: login: aaperis
body: Works great! :smile: 
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T10:30:53Z
user: login: jbygdell
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T10:16:51Z
user: login: jbygdell
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-14T11:57:02Z
user: login: nanjiangshu
body: Looks good. Good job!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T15:52:25Z

Review comments:
diff_hunk: @@ -118,13 +118,16 @@ func (auth AuthHandler) postEGA(ctx iris.Context) {
 
 	res, err := authenticateWithCEGA(auth.Config.Cega, username)
 
+	var statusCode int
 	if err != nil {
 		log.Error(err)
+		statusCode = 404
+	} else {
+		statusCode = res.StatusCode
+		defer res.Body.Close()
path: sda/cmd/auth/main.go
user: login: jbygdell
body: ```suggestion
	if err != nil {
		log.Errorf("cega connection error: %v",err)
		res = &http.Response{
			Body: io.NopCloser(nil),
			StatusCode: http.StatusInternalServerError,
		}
```

This also means that the 404 case below needs to be replaced with:
`case 500,502,503:` so that we cover all bases when the NSS server misbehaves.
created_at: 2025-03-10T10:12:52Z
author_association: COLLABORATOR
diff_hunk: @@ -118,13 +118,16 @@ func (auth AuthHandler) postEGA(ctx iris.Context) {
 
 	res, err := authenticateWithCEGA(auth.Config.Cega, username)
 
+	var statusCode int
 	if err != nil {
 		log.Error(err)
+		statusCode = 404
+	} else {
+		statusCode = res.StatusCode
+		defer res.Body.Close()
 	}
 
-	defer res.Body.Close()
-
-	switch res.StatusCode {
+	switch statusCode {
path: sda/cmd/auth/main.go
user: login: jbygdell
body: ```suggestion
	defer res.Body.Close()

	switch res.StatusCode {
```
created_at: 2025-03-10T10:13:29Z
author_association: COLLABORATOR

PR comments:
user: login: MalinAhlberg
created_at: 2025-03-10T08:10:41Z
body: > Works great! 😄

Thanks for the quick review! I realized there were some more problems though, they are now fixed in bd52543  and 45654b4.
author_association: CONTRIBUTOR
user: login: MalinAhlberg
created_at: 2025-03-14T11:34:57Z
body: I did the refactoring suggested by @jbygdell in a7999d3 and added some extra checks b456ef0. The last commit is another small refactoring, everything else is just rebasing.
author_association: CONTRIBUTOR