Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions SECURE_DATA_FLOW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Secure Data Flow: Backend to Frontend

## Overview

This document explains how sensitive gin.H data is securely passed from `main.go` to `indexPage.js` when `index.html` first loads.

## Data Flow Diagram

```@text
1. Browser requests "/"
2. main.go "/" handler:
- Calls getUserRoleInfo() → authenticates user via SSOP
- Generates temporary token (TempToken) with expiration
- Calls GetFormTemplates() → retrieves available forms
- Creates gin.H with non-sensitive data + TempToken
3. Renders index.html template with gin.H data embedded
- {{.TempToken}} → embedded as window.userToken
- {{.UserEmail}} → embedded as window.userEmail
- {{.UserRole}} → embedded as window.authenticatedRole
4. JavaScript (indexPage.js) on page load:
- Accesses window.userToken from template variable
- Uses token to fetch additional sensitive data:
* /allFieldIds?token=...
* /formElementPermissions?token=...
- All subsequent API calls include token in query params
```

## Security Features

### Token Storage

- **Location**: `window.userToken` (memory only)
- **Persistence**: Lost on page refresh (requires re-authentication)
- **Transmission**: Only over HTTPS
- **Validation**: Server validates token on every request via `checkToken()` function

### Data Categories

#### Template-Embedded (Non-Sensitive)

These are embedded directly in HTML from `gin.H`:

- `RootPath` - Application root path for routing
- `UserEmail` - User's email address
- `UserRole` - User's authentication role (vx, dsg, mdev)
- `ReturnURL` - Post-authentication redirect URL
- Static resource paths (images, logos)

#### Token-Protected (Sensitive)

These require the temporary token to fetch:

- Form element permissions (which UI elements to show/hide/disable)
- Field IDs for form elements
- Form-specific data
- Job set configurations

## Implementation Details

### Backend (main.go)

The "/" route endpoint:

```go
api.GET("/", func(c *gin.Context) {
userRoleInfo, tempToken, shouldReturn := getUserRoleInfo(c)
// ... validation ...

data := gin.H{
"TempToken": tempToken, // Temporary token for subsequent requests
"UserRole": userRoleInfo.UserRole,
"UserEmail": userRoleInfo.UserInfo.Email,
"forms": templates, // List of available forms
// ... other data ...
}
c.HTML(http.StatusOK, "index.html", data)
})
```

#### API authorization

Each user role is authorized for a collection of API routes. The api route path and the user role are checked against an APIPathPermissionsForRole structure that is retrieved from the database. The javascript code should prevent inadvertent attempts to access a protected route (such as one that would upsert the database) but that would not prevent an attempt to access the protected route independently in the case that a token had been somehow compromised. The role / path authorization would prevent that.

### Frontend (index.html)

Template variables excerpt:

```@html
<script>
window.ROOT_PATH = "{{.RootPath}}";
window.authenticatedRole = "{{.UserRole}}";
window.userEmail = "{{.UserEmail}}";
window.userToken = "{{.TempToken}}"; // Secure token in memory only

if (!window.userToken || window.userToken.trim() === "") {
document.getElementById("youNeedToAuthenticate").style.display = "block";
} else {
document.getElementById("mainContent").style.display = "flex";
}
</script>
```

### JavaScript (indexPage.js)

Fetching token-protected data:

```javascript
async function checkAllowed() {
// Fetch using token stored from template variable
const [fieldIdsData, permissionsData] = await Promise.all([
fetch(window.ROOT_PATH + `/allFieldIds?token=${encodeURIComponent(window.userToken)}`),
fetch(window.ROOT_PATH + `/formElementPermissions?token=${encodeURIComponent(window.userToken)}`)
]);
// ... process permissions ...
}
```

## Token Lifetime

1. **Generated**: When user accesses "/" after authentication
2. **Transmitted**: Embedded in HTML template at render time
3. **Available**: Accessible via `window.userToken` on page load
4. **Validated**: Server checks token on every API request
5. **Expired**: Lost when page refreshes (user must re-authenticate)

## Security Checklist

- ✅ Token generated server-side with expiration
- ✅ Token transmitted over HTTPS only
- ✅ Token stored in memory (not in localStorage/sessionStorage)
- ✅ Token lost on page refresh (forces re-authentication)
- ✅ Token validated on every backend request
- ✅ Non-sensitive data embedded in HTML
- ✅ Sensitive data fetched via token-protected endpoints

## Best Practices

1. **Never expose token in logs**: Token contains sensitive auth info
2. **Always use HTTPS**: Token must be transmitted over secure channels
3. **Short expiration time**: Token should expire in minutes (5-15 min recommended)
4. **Validate on every request**: Backend must check token validity
5. **Clear on logout**: Token should be invalidated on logout
6. **Rotate tokens**: Generate new token on each page load

## Adding New Data

### If data is public/non-sensitive

1. Add to `gin.H` in "/" route
2. Embed in template: `{{.FieldName}}`
3. Access in JavaScript: `window.fieldName`

### If data is sensitive

1. Create new endpoint requiring token parameter
2. Validate token via `checkToken()` function
3. Return data as JSON
4. Fetch from JavaScript with `window.userToken`
61 changes: 52 additions & 9 deletions fernet.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,74 @@ import (
"github.com/fernet/fernet-go"
)

func decryptFernetToken(accessToken, fernetKey string, maxAge int) (*UserInfo, error) {
func decryptFernetToken(accessToken, fernetKey string, maxAge int) (UserInfo, error) {
// Parse the Fernet key
key, err := fernet.DecodeKey(fernetKey)
if err != nil {
return nil, fmt.Errorf("invalid fernet key: %w", err)
return UserInfo{}, fmt.Errorf("invalid fernet key: %w", err)
}

// Decrypt the token
decrypted := fernet.VerifyAndDecrypt([]byte(accessToken), time.Duration(maxAge)*time.Second, []*fernet.Key{key})
if decrypted == nil {
return nil, fmt.Errorf("failed to decrypt access_token - invalid token or key")
return UserInfo{}, fmt.Errorf("failed to decrypt access_token - invalid token or key")
}

fmt.Printf("[DEBUG] \n\nDecrypted token: %s\n\n", string(decrypted))

// The decrypted payload is a JSON array with a single object
var payload []UserInfo
err = json.Unmarshal(decrypted, &payload)
var userInfo []UserInfo
err = json.Unmarshal(decrypted, &userInfo)
if err != nil {
return nil, fmt.Errorf("failed to parse decrypted JSON: %w (raw data: %s)", err, string(decrypted))
return UserInfo{}, fmt.Errorf("failed to parse decrypted JSON: %w (raw data: %s)", err, string(decrypted))
}

if len(payload) == 0 {
return nil, fmt.Errorf("empty payload in decrypted token")
if len(userInfo) == 0 {
return UserInfo{}, fmt.Errorf("empty payload in decrypted token")
}

return &payload[0], nil
return userInfo[0], nil
}

func generateTemporaryFernetToken(userRoleInfo UserRoleInfo, fernetKey string) (string, error) {
// Parse the Fernet key
key, err := fernet.DecodeKey(fernetKey)
if err != nil {
return "", fmt.Errorf("invalid fernet key: %w", err)
}

// Create the payload as a JSON array with a single object
userRoleInfoData, err := json.Marshal([]UserRoleInfo{userRoleInfo})
if err != nil {
return "", fmt.Errorf("failed to marshal user info to JSON: %w", err)
}

fmt.Printf("[DEBUG] \n\nuserRoleInfoData for token generation: %s\n\n", string(userRoleInfoData))

// Encrypt the userRoleInfoData
token, err := fernet.EncryptAndSign(userRoleInfoData, key)
if err != nil {
return "", fmt.Errorf("failed to encrypt userRoleInfoData: %w", err)
}
return string(token), nil
}

func VerifyToken(token string, maxAge time.Duration, fernetKey string) (UserRoleInfo, error) {
key, err := fernet.DecodeKey(fernetKey)
if err != nil {
return UserRoleInfo{}, fmt.Errorf("invalid fernet key: %w", err)
}
decrypted := fernet.VerifyAndDecrypt([]byte(token), maxAge, []*fernet.Key{key})
if decrypted == nil {
return UserRoleInfo{}, fmt.Errorf("failed to decrypt token - invalid token or key")
}
var userRoleInfo []UserRoleInfo
err = json.Unmarshal(decrypted, &userRoleInfo)
if err != nil {
return UserRoleInfo{}, err
}
if len(userRoleInfo) == 0 {
return UserRoleInfo{}, fmt.Errorf("empty userRoleInfo in decrypted token")
}
return userRoleInfo[0], nil
}
7 changes: 3 additions & 4 deletions fernet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,9 @@ func TestFernet(t *testing.T) {
t.Skip("FERNET_KEY environment variable not set, skipping test") // Skip the test if FERNET_KEY is not set
return
}
//[{"email": "renn.valo@noaa.gov", "given_name": "", "family_name": "", "return_url": "https://apps-dev.gsd.esrl.noaa.gov/vx/formsui/", "return_html": "https://apps-dev.gsd.esrl.noaa.gov/vx/formsui/"}]
payload_mdev := []byte(`[{"Email": "jason.english@noaa.gov", "GivenName": "", "FamilyName": "", "ReturnURL": "https://apps-dev.gsd.esrl.noaa.gov/vx/formsui/", "ReturnHTML": "https://apps-dev.gsd.esrl.noaa.gov/vx/formsui/"}]`)
payload_dsg := []byte(`[{"Email": "greg.pratt@noaa.gov", "GivenName": "", "FamilyName": "", "ReturnURL": "https://apps-dev.gsd.esrl.noaa.gov/vx/formsui/", "ReturnHTML": "https://apps-dev.gsd.esrl.noaa.gov/vx/formsui/"}]`)
payload_vx := []byte(`[{"Email": "randy.pierce@noaa.gov", "GivenName": "", "FamilyName": "", "ReturnURL": "https://apps-dev.gsd.esrl.noaa.gov/vx/formsui/", "ReturnHTML": "https://apps-dev.gsd.esrl.noaa.gov/vx/formsui/"}]`)
payload_mdev := []byte(`[{"Email": "mdev@noaa.gov", "GivenName": "", "FamilyName": "", "ReturnURL": "http://localhost:8080/vx/formsui/", "ReturnHTML": "http://localhost:8080/vx/formsui/"}]`)
payload_dsg := []byte(`[{"Email": "dsg@noaa.gov", "GivenName": "", "FamilyName": "", "ReturnURL": "http://localhost:8080/vx/formsui/", "ReturnHTML": "http://localhost:8080/vx/formsui/"}]`)
payload_vx := []byte(`[{"Email": "vx@noaa.gov", "GivenName": "", "FamilyName": "", "ReturnURL": "http://localhost:8080/vx/formsui/", "ReturnHTML": "http://localhost:8080/vx/formsui/"}]`)
key, err := fernet.DecodeKey(encodedKey)
if err != nil {
panic(fmt.Sprintf("Failed to decode FERNET_KEY: %v", err))
Expand Down
Loading