Skip to content

Commit

Permalink
feat: implement authentication and authorization (#217)
Browse files Browse the repository at this point in the history
* implement authentication ang authorization

Signed-off-by: Patricia Reinoso <[email protected]>

* refactoring

Signed-off-by: Patricia Reinoso <[email protected]>

* fix circular dependency

Signed-off-by: Patricia Reinoso <[email protected]>

* fix auth db mistake

Signed-off-by: Patricia Reinoso <[email protected]>

* fix lint issues

Signed-off-by: Patricia Reinoso <[email protected]>

* use index on username for useraccount collection

Signed-off-by: Patricia Reinoso <[email protected]>

* fix login path to be compatible with ui

Signed-off-by: Patricia Reinoso <[email protected]>

* refactor tests and minor comments

Signed-off-by: Patricia Reinoso <[email protected]>

* renaming according to review comments

Signed-off-by: Patricia Reinoso <[email protected]>

* refactor user types according to the action

Signed-off-by: Patricia Reinoso <[email protected]>

* refactor to simplify authorization logic

Signed-off-by: Patricia Reinoso <[email protected]>

* add status endpoint

Signed-off-by: Patricia Reinoso <[email protected]>

* add account method to dbadapter and use it to know the number of user accounts created

Signed-off-by: Patricia Reinoso <[email protected]>

* refactoring: package reorganization

Signed-off-by: Patricia Reinoso <[email protected]>

* refactoring: reorganizing. moving login endpoint to auth

Signed-off-by: Patricia Reinoso <[email protected]>

* minor comments fixes

Signed-off-by: Patricia Reinoso <[email protected]>

* fix lint job

Signed-off-by: Patricia Reinoso <[email protected]>

* add readme information

Signed-off-by: Patricia Reinoso <[email protected]>

* fix frontend conflict on paths

Signed-off-by: Patricia Reinoso <[email protected]>

* fix method signature update

Co-authored-by: Kayra <[email protected]>
Signed-off-by: Patricia Reinoso <[email protected]>

* fix use forbidden instead of unauthorized when the user does not have permission

Signed-off-by: Patricia Reinoso <[email protected]>

* add copyright and release version

Signed-off-by: Patricia Reinoso <[email protected]>

* fix copyright format

Signed-off-by: Patricia Reinoso <[email protected]>

* Update backend/auth/README.md

Co-authored-by: gab-arrobo <[email protected]>
Signed-off-by: Patricia Reinoso <[email protected]>

* Update backend/auth/README.md

Co-authored-by: gab-arrobo <[email protected]>
Signed-off-by: Patricia Reinoso <[email protected]>

* Update backend/auth/README.md

Co-authored-by: gab-arrobo <[email protected]>
Signed-off-by: Patricia Reinoso <[email protected]>

* Update backend/auth/README.md

Co-authored-by: gab-arrobo <[email protected]>
Signed-off-by: Patricia Reinoso <[email protected]>

* small fixes from review comments

Signed-off-by: Patricia Reinoso <[email protected]>

* add doc for swagger

Signed-off-by: Patricia Reinoso <[email protected]>

* add license

Signed-off-by: Patricia Reinoso <[email protected]>

* fix swagger doc

Signed-off-by: Patricia Reinoso <[email protected]>

* minor fixes from comments

Signed-off-by: Patricia Reinoso <[email protected]>

---------

Signed-off-by: Patricia Reinoso <[email protected]>
Co-authored-by: Kayra <[email protected]>
Co-authored-by: gab-arrobo <[email protected]>
  • Loading branch information
3 people authored Oct 24, 2024
1 parent d8abc03 commit 4761989
Show file tree
Hide file tree
Showing 32 changed files with 3,367 additions and 421 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ public/
!/frontend/public/*.license
.coverage/
vendor/
config/webuicfg.yaml
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!--
# SPDX-FileCopyrightText: 2021 Open Networking Foundation <[email protected]>
# Copyright 2019 free5GC.org
SPDX-FileCopyrightText: 2021 Open Networking Foundation <[email protected]>
Copyright 2019 free5GC.org
SPDX-FileCopyrightText: 2024 Canonical Ltd.
SPDX-License-Identifier: Apache-2.0
-->
[![Go Report Card](https://goreportcard.com/badge/github.com/omec-project/webconsole)](https://goreportcard.com/report/github.com/omec-project/webconsole)
Expand All @@ -17,7 +17,7 @@ features Configuration Service provides APIs for subscriber management.
4. 5G clients can connect & get complete configuration copy through grpc interface.
5. 4G clients communicate with Webconsole through REST interface

# UI
## UI

Webconsole can optionally serve static files, which constitute the frontend part of the application.

Expand All @@ -35,6 +35,12 @@ http://<webconsole-ip>:5000/

An example static file has been placed in the `webconsole/ui/frontend_files` directory.

## Authentication and Authorization

The authentication and authorization feature ensures that only verified and authorized users can access the webui resources and interact with the system.

This is an optional feature, disabled by default. For more details, refer to this [file](backend/auth/README.md).

## Webconsole Architecture diagram

![Architecture](/docs/images/architecture1.png)
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.5.1-dev
1.6.0
156 changes: 156 additions & 0 deletions backend/auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2024 Canonical Ltd
-->

# Authentication and Authorization Feature

Webui is the upstream component that offers an API to configure the 5G core network in Aether SD-Core. With the implementation of the Authentication and Authorization feature, security risks have been reduced, ensuring that access is restricted to authorized users only.

This is an optional feature that is disabled by default.

## The Feature

JWT is used to authenticate users, ensuring secure access to the system. For protected endpoints, users must include a `token`, which Webui uses to verify their identity and grant access.

Authorization is implemented based on these 2 roles: `AdminRole` and `UserRole`. The webui uses the `token` to determine the role of the user performing the action.

Depending on their role, users have different levels of access to Webui operations:

- `UserRole`: Users with this role can retrieve their own account information and change their own password.
- `AdminRole`: Admin users have full access to all endpoints, allowing them to perform any action on their own account as well as on other users' accounts.

Both `AdminRole` and `UserRole` users can manage additional resources, such as Network Slices, Device Groups, and Subscribers, but they must be logged in.

The `AdminRole` user cannot be deleted.

## Setup

### Enable the Feature

To enable this feature, add the following parameters to the config file.
```
configuration:
enableAuthentication: true
mongodb:
. . .
webuiDbName: <name>
webuiDbUrl: <url>
```

### First User Creation

On a fresh deployment, the endpoint for creating a new user is not protected, allowing initial setup without authentication:

```
curl -v "localhost:5000/config/v1/account" \
--data '{
"username": <username>,
"password": <password>
}'
```

The first user created will automatically be assigned the `AdminRole`. Only one user can hold the `AdminRole`, and this user cannot be deleted.

For all subsequent user creations, authentication is required by logging in.

## Endpoints that does not require authorization

There are three endpoints that can be accessed without providing a JWT token in the request header: [Log in](#log-in), [Get Status](#get-status), [First User Creation](#first-user-creation)

### Log in

The log in operation is required before performing most actions on the Webui. It generates a `token`, which must be included in subsequent requests for authentication.

```
curl -v "localhost:5000/login" \
--header 'Content-Type: application/json' \
--data '{
"username": <username>,
"password": <password>
}'
```
Response:
```
{"token":"eyJhbG123aad1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluVXNlciIsInBlcm1pc3Npb25zIjoxLCJleHAiOjE3MjY1ODIyNTZ9.YU6tveV3oXcfGMvqB7xIcP1Fs6c6ZZoP134Y8ozV4lA"}
```

### Get Status
This endpoint allows any user to check the status of the Webui without a `token`. The status indicates whether the system's initialization has occurred, specifically whether the first `AdminRole` user has been created, and if the system is ready to use.

```
curl -v "localhost:5000/status"
```
Response:
```
{"initialized":false}
```
or
```
{"initialized":true}
```

### First User Creation

As mentioned above, the [First User Creation](#first-user-creation) endpoint does not require a `JWT token`, allowing initial setup without authentication.

## User Management Endpoints

To access any of the following endpoints, users must be logged in and include a valid JWT token in the request header.

### Create User
Create a new user by providing the username and password.
```
curl -v -H "Authorization: Bearer <token>" "localhost:5000/config/v1/account" \
--data '{
"username": <username>,
"password": <password>
}'
```

### Get Users
Retrieve a list of all users.
```
curl -v -H "Authorization: Bearer <token>" "localhost:5000/config/v1/account"
```
Response:
```
[{"username":"adminUser","role":1}]
```

### Get User
Retrieve details for a specific user by their username.
```
curl -v -H "Authorization: Bearer <token>" "localhost:5000/config/v1/account/<username>"
```
Response:
```
{"username":"adminUser","role":1}
```

### Change Password
Change the password for a specific user.
```
curl -v -H "Authorization: Bearer <token>" "localhost:5000/config/v1/account/<username>/change_password" \
--data '{
"password": <new_password>
}'
```

### Delete User
Delete a specific user by their username.
```
curl -v -H "Authorization: Bearer <token>" -X DELETE "localhost:5000/v1/config/account/<username>"
```

## Other Endpoints

Configuration endpoints now require the inclusion of a JWT token in the request header for authorization.
```
curl -v -H "Authorization: Bearer <token>" "localhost:5000/api/subscriber/<imsi>" \
--header 'Content-Type: text/plain' \
--data '{
...
}'
```
118 changes: 118 additions & 0 deletions backend/auth/handlers_login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2024 Canonical Ltd

package auth

import (
"encoding/json"
"net/http"
"time"

"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"github.com/omec-project/webconsole/backend/logger"
"github.com/omec-project/webconsole/configmodels"
"github.com/omec-project/webconsole/dbadapter"
"go.mongodb.org/mongo-driver/bson"
"golang.org/x/crypto/bcrypt"
)

const (
errorIncorrectCredentials = "incorrect username or password. Try again"
errorInvalidDataProvided = "invalid data provided"
errorLogin = "failed to log in"
errorMissingPassword = "password is required"
errorMissingUsername = "username is required"
errorRetrieveUserAccount = "failed to retrieve user account"
)

type LoginParams struct {
Username string `json:"username"`
Password string `json:"password"`
}

type LoginResponse struct {
Token string `json:"token"`
}

// LoginPost godoc
//
// @Description Log in. Only available if enableAuthentication is enabled.
// @Tags Auth
// @Param loginParams body LoginParams true " "
// @Success 200 {object} LoginResponse "Authorization token"
// @Failure 400 {object} nil "Bad request"
// @Failure 401 {object} nil "Authentication failed"
// @Failure 404 {object} nil "Page not found if enableAuthentication is disabled"
// @Failure 500 {object} nil "Internal server error"
// @Router /login [post]
func Login(jwtSecret []byte) gin.HandlerFunc {
return func(c *gin.Context) {
var loginParams LoginParams
err := c.ShouldBindJSON(&loginParams)
if err != nil {
logger.AuthLog.Errorln(err.Error())
c.JSON(http.StatusBadRequest, gin.H{"error": errorInvalidDataProvided})
return
}
if loginParams.Username == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": errorMissingUsername})
return
}
if loginParams.Password == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": errorMissingPassword})
return
}

filter := bson.M{"username": loginParams.Username}
rawUserAccount, err := dbadapter.WebuiDBClient.RestfulAPIGetOne(configmodels.UserAccountDataColl, filter)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": errorRetrieveUserAccount})
return
}
if len(rawUserAccount) == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"error": errorIncorrectCredentials})
return
}
var dbUser configmodels.DBUserAccount
err = json.Unmarshal(configmodels.MapToByte(rawUserAccount), &dbUser)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": errorRetrieveUserAccount})
return
}
if err = bcrypt.CompareHashAndPassword([]byte(dbUser.HashedPassword), []byte(loginParams.Password)); err != nil {
logger.AuthLog.Errorln(err.Error())
c.JSON(http.StatusUnauthorized, gin.H{"error": errorIncorrectCredentials})
return
}
token, err := GenerateJWT(dbUser.Username, dbUser.Role, jwtSecret)
if err != nil {
logger.AuthLog.Errorln(err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"error": errorLogin})
return
}
loginResponse := LoginResponse{
Token: token,
}
c.JSON(http.StatusOK, loginResponse)
}
}

func expireAfter() int64 {
return time.Now().Add(time.Hour * 1).Unix()
}

func GenerateJWT(username string, role int, jwtSecret []byte) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwtWebconsoleClaims{
Username: username,
Role: role,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expireAfter(),
},
})
tokenString, err := token.SignedString(jwtSecret)
if err != nil {
return "", err
}
return tokenString, nil
}
Loading

0 comments on commit 4761989

Please sign in to comment.