Schiefelbein, Andrew c13a04669b Add string constants and improve the default catch of statistics
1.  Move ephemeral strings to constants where appropriate
2.  Change the switch to a regex to catch the defaults in stats
3.  Renamed the websocket modules to match with the backend

Change-Id: I9e756bb046c78e6aef393adb83db2d22ebc6a585
2020-11-06 13:09:58 -06:00

194 lines
5.5 KiB
Go
Executable File

/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package webservice
import (
"crypto/sha512"
"encoding/hex"
"errors"
"fmt"
"time"
"github.com/dgrijalva/jwt-go"
"opendev.org/airship/airshipui/pkg/configs"
"opendev.org/airship/airshipui/pkg/log"
)
// Create the JWT key used to create the signature
// TODO: use a private key for this instead of a phrase
var jwtKey = []byte("airshipUI_JWT_key")
const (
username = "username"
password = "password"
expiration = "exp"
)
// The UI will either request authentication or validation, handle those situations here
func handleAuth(_ *string, request configs.WsMessage) configs.WsMessage {
response := configs.WsMessage{
Type: configs.UI,
Component: configs.Auth,
}
var err error
switch request.SubComponent {
case configs.Authenticate:
if request.Authentication != nil {
var token *string
authRequest := request.Authentication
token, err = createToken(authRequest.ID, authRequest.Password)
if token != nil {
sessions[request.SessionID].jwt = *token
response.SubComponent = configs.Approved
response.Token = token
}
} else {
err = errors.New("No AuthRequest found in the request")
}
case configs.Validate:
if request.Token != nil {
_, err = validateToken(request)
response.SubComponent = configs.Approved
response.Token = request.Token
} else {
err = errors.New("No token found in the request")
}
default:
err = errors.New("Invalid authentication request")
}
if err != nil {
log.Error(err)
e := err.Error()
response.Error = &e
response.SubComponent = configs.Denied
}
return response
}
// validate JWT (JSON Web Token)
func validateToken(request configs.WsMessage) (*string, error) {
// update the token string to be the refresh token if it's present
// otherwise just use the default token string
// TODO(aschiefe): determine if we need to compare the original token claims to the refresh
tokenString := request.Token
if request.RefreshToken != nil {
tokenString = request.RefreshToken
}
token, err := jwt.Parse(*tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return jwtKey, nil
})
if err != nil {
log.Error(err)
return nil, err
}
// extract the claim from the token
if claim, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
// extract the user from the claim
if user, ok := claim[username].(string); ok {
// test to see if we need to sent a refresh token
go testForRefresh(claim, request)
return &user, nil
}
err = errors.New("Invalid JWT User")
log.Error(err)
return nil, err
}
err = errors.New("Invalid JWT Token")
log.Error(err)
return nil, err
}
// create a JWT (JSON Web Token)
func createToken(id string, passwd string) (*string, error) {
origPasswdHash, ok := configs.UIConfig.Users[id]
if !ok {
return nil, errors.New("Not authenticated")
}
// test the password to make sure it's valid
hash := sha512.New()
_, err := hash.Write([]byte(passwd))
if err != nil {
return nil, errors.New("Error authenticating")
}
if origPasswdHash != hex.EncodeToString(hash.Sum(nil)) {
return nil, errors.New("Not authenticated")
}
// set some claims
claims := make(jwt.MapClaims)
claims[username] = id
claims[password] = passwd
claims[expiration] = time.Now().Add(time.Hour * 1).Unix()
// create the token
jwtClaim := jwt.New(jwt.SigningMethodHS256)
jwtClaim.Claims = claims
// Sign and get the complete encoded token as string
token, err := jwtClaim.SignedString(jwtKey)
return &token, err
}
// from time to time we might want to send a refresh token to the UI. The UI should not be in charge of requesting it
func testForRefresh(claim jwt.MapClaims, request configs.WsMessage) {
// for some reason the exp is stored as an float and not an int in the claim conversion
// so we do a little dance and cast some floats to ints and everyone goes on with their lives
if exp, ok := claim[expiration].(float64); ok {
if int64(exp) < time.Now().Add(time.Minute*15).Unix() {
createRefreshToken(claim, request)
}
}
}
// createRefreshToken will create an oauth2 refresh token based on the timeout on the UI
func createRefreshToken(claim jwt.MapClaims, request configs.WsMessage) {
// add the new expiration to the claim
claim[expiration] = time.Now().Add(time.Hour * 1).Unix()
// create the token
jwtClaim := jwt.New(jwt.SigningMethodHS256)
jwtClaim.Claims = claim
// Sign and get the complete encoded token as string
refreshToken, err := jwtClaim.SignedString(jwtKey)
if err != nil {
log.Error(err)
return
}
// test to see if the session is still in existence before firing off a message
if session, ok := sessions[request.SessionID]; ok {
if err = session.webSocketSend(configs.WsMessage{
Type: configs.UI,
Component: configs.Auth,
SubComponent: configs.Refresh,
RefreshToken: &refreshToken,
SessionID: request.SessionID,
}); err != nil {
session.onError(err)
}
}
}