Skip to content

Commit

Permalink
Merge pull request #14 from Excubitor-Monitoring/Implement-PAM-Authen…
Browse files Browse the repository at this point in the history
…tication

Implement PAM authentication
  • Loading branch information
Uggah authored Apr 22, 2023
2 parents b0c5838 + 9e76eb6 commit 15a7c11
Show file tree
Hide file tree
Showing 10 changed files with 1,041 additions and 5 deletions.
2 changes: 2 additions & 0 deletions cmd/excubitor/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ func initConfig() error {
viper.SetDefault("logging.method", "CONSOLE")
viper.SetDefault("http.port", 8080)
viper.SetDefault("http.host", "localhost")
viper.SetDefault("http.cors.allowed_origin", "*")
viper.SetDefault("http.auth.jwt.secret", "")

if _, err := os.Stat("config.yml"); errors.Is(err, fs.ErrNotExist) {
err := viper.WriteConfig()
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ require (
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/golang-jwt/jwt/v5 v5.0.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/msteinert/pam v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/afero v1.9.3 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.2.0 h1:u0p9s3xLYpZCA1z5JgCkMeB34CKCMMQbM+G8Ii7YD0I=
github.com/gobwas/ws v1.2.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
Expand Down Expand Up @@ -142,6 +144,8 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/msteinert/pam v1.1.0 h1:VhLun/0n0kQYxiRBJJvVpC2jR6d21SWJFjpvUVj20Kc=
github.com/msteinert/pam v1.1.0/go.mod h1:M4FPeAW8g2ITO68W8gACDz13NDJyOQM9IQsQhrR6TOI=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
Expand Down
232 changes: 232 additions & 0 deletions internal/http_server/authentication.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
package http_server

import (
"encoding/json"
"errors"
"fmt"
"github.com/Excubitor-Monitoring/Excubitor-Backend/internal/pam"
"github.com/golang-jwt/jwt/v5"
"github.com/spf13/viper"
"io"
"net/http"
"strings"
"time"
)

type Credentials interface {
Authenticate() bool
}

type authRequest struct {
Method string `json:"method"`
Credentials map[string]interface{} `json:"credentials"`
}

type authResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}

type refreshResponse struct {
AccessToken string `json:"access_token"`
}

func handleAuthRequest(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")

if r.Method != http.MethodPost {
ReturnError(w, r, http.StatusMethodNotAllowed, "Method is not allowed!")
return
}

if r.Body != nil {
bytes, err := io.ReadAll(r.Body)
if err != nil {
logger.Debug(fmt.Sprintf("Couldn't read message body of auth request from %s", r.RemoteAddr))
ReturnError(w, r, http.StatusBadRequest, "Can't read message body!")
return
}

request := &authRequest{}
err = json.Unmarshal(bytes, request)
if err != nil {
logger.Debug(fmt.Sprintf("Couldn't decode message body of auth request from %s", r.RemoteAddr))
ReturnError(w, r, http.StatusBadRequest, "Can't decode message body!")
return
}

switch request.Method {
case "PAM":
username := request.Credentials["username"].(string)
password := request.Credentials["password"].(string)

pamCredentials := pam.PAMPasswordCredentials{Username: username, Password: password}

if pamCredentials.Authenticate() {
accessTokenClaims := jwt.MapClaims{
"iss": "excubitor-backend",
"sub": username,
"exp": time.Now().Add(30 * time.Minute).Unix(),
}

accessToken, err := signAccessToken(accessTokenClaims)
if err != nil {
logger.Error(fmt.Sprintf("Couldn't sign access token for %s! Reason: %s", r.RemoteAddr, err))
ReturnError(w, r, http.StatusInternalServerError, "Internal Server Error!")
return
}

refreshTokenClaims := jwt.MapClaims{
"iss": "excubitor-backend",
"sub": username,
"exp": time.Now().Add(4 * time.Hour).Unix(),
}

refreshToken, err := signRefreshToken(refreshTokenClaims)
if err != nil {
logger.Error(fmt.Sprintf("Couldn't sign refresh token for %s! Reason: %s", r.RemoteAddr, err))
ReturnError(w, r, http.StatusInternalServerError, "Internal Server Error!")
return
}

tokens := &authResponse{
accessToken,
refreshToken,
}

jsonResponse, err := json.Marshal(tokens)
if err != nil {
logger.Error(fmt.Sprintf("Couldn't assemble json response for auth request from %s.", r.RemoteAddr))
ReturnError(w, r, http.StatusInternalServerError, "Internal Server Error!")
return
}

w.WriteHeader(http.StatusOK)
_, err = w.Write(jsonResponse)
if err != nil {
return
}
} else {
ReturnError(w, r, http.StatusUnauthorized, "Invalid username or password!")
return
}
default:
ReturnError(w, r, http.StatusBadRequest, "Unsupported authentication method: "+request.Method)
return
}
}
}

func handleRefreshRequest(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")

if r.Method != http.MethodPost {
ReturnError(w, r, http.StatusMethodNotAllowed, "Method is not allowed!")
return
}

authorization := r.Header.Get("Authorization")

if !strings.HasPrefix(authorization, "Bearer ") {
w.Header().Set("WWW-Authenticate", "Bearer")
ReturnError(w, r, http.StatusUnauthorized, "Bearer authentication is needed!")
return
}

token := strings.Split(authorization, "Bearer ")[1]

jwtToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
return []byte(viper.GetString("http.auth.jwt.refreshTokenSecret")), nil
}, jwt.WithValidMethods([]string{"HS256"}), jwt.WithIssuer("excubitor-backend"))

if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) {
logger.Debug(fmt.Sprintf("Attempt to refresh access token with expired token from %s!", r.RemoteAddr))
ReturnError(w, r, http.StatusUnauthorized, "Token expired!")
return
} else if errors.Is(err, jwt.ErrSignatureInvalid) {
logger.Warn(fmt.Sprintf("Attempt to authenticate with invalid signature from %s!", r.RemoteAddr))
} else {
logger.Debug(fmt.Sprintf("Attempt to authenticate with invalid token from %s! Reason: %s", r.RemoteAddr, err))
}

ReturnError(w, r, http.StatusUnauthorized, "Invalid token!")
return
}

username, err := jwtToken.Claims.GetSubject()
if err != nil {
logger.Warn(fmt.Sprintf("Couldn't read subject claim of refresh token from %s! Reason: %s", r.RemoteAddr, err))
ReturnError(w, r, http.StatusBadRequest, "Token has no subject!")
return
}

accessTokenClaims := jwt.MapClaims{
"iss": "excubitor-backend",
"sub": username,
"exp": time.Now().Add(30 * time.Minute).Unix(),
}

accessToken, err := signAccessToken(accessTokenClaims)
if err != nil {
logger.Error(fmt.Sprintf("Couldn't sign access token for %s! Reason: %s", r.RemoteAddr, err))
ReturnError(w, r, http.StatusInternalServerError, "Internal Server Error!")
return
}

jsonResponse, err := json.Marshal(refreshResponse{accessToken})
if err != nil {
logger.Error(fmt.Sprintf("Couldn't encode access token for %s! Reason: %s", r.RequestURI, err))
ReturnError(w, r, http.StatusInternalServerError, "Internal Server Error!")
return
}

w.WriteHeader(http.StatusOK)
_, err = w.Write(jsonResponse)
if err != nil {
logger.Error(fmt.Sprintf("Couldn't send access token to %s! Reason: %s", r.RemoteAddr, err))
return
}
}

func auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authorization := r.Header.Get("Authorization")

if !strings.HasPrefix(authorization, "Bearer ") {
w.Header().Set("WWW-Authenticate", "Bearer")
ReturnError(w, r, http.StatusUnauthorized, "Bearer authentication is needed!")
return
}

token := strings.Split(authorization, "Bearer ")[1]

jwtToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
return []byte(viper.GetString("http.auth.jwt.accessTokenSecret")), nil
}, jwt.WithValidMethods([]string{"HS256"}), jwt.WithIssuer("excubitor-backend"))

if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) {
logger.Debug(fmt.Sprintf("Attempt to authenticate with expired token from %s!", r.RemoteAddr))
ReturnError(w, r, http.StatusUnauthorized, "Token expired!")
return
} else if errors.Is(err, jwt.ErrSignatureInvalid) {
logger.Warn(fmt.Sprintf("Attempt to authenticate with invalid signature from %s!", r.RemoteAddr))
} else {
logger.Debug(fmt.Sprintf("Attempt to authenticate with invalid token from %s! Reason: %s", r.RemoteAddr, err))
}

ReturnError(w, r, http.StatusUnauthorized, "Invalid token!")
return
}

user, err := jwtToken.Claims.GetSubject()
if err != nil {
logger.Warn(fmt.Sprintf("Couldn't read token subject from %s!", user))
}

logger.Trace(fmt.Sprintf("User %s authenticated successfully using JWT token!", user))

next.ServeHTTP(w, r)
})
}
Loading

0 comments on commit 15a7c11

Please sign in to comment.