
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
194 lines
5.5 KiB
Go
Executable File
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)
|
|
}
|
|
}
|
|
}
|