add support for asymmetric crypto based jwt signatures (#145)
* add support for asymmetric crypto based jwt signatures * wip: e2e test example * wip: rsa key parse error... * fix: test works --------- Co-authored-by: James Read <contact@jread.com>
This commit is contained in:
parent
8c1c0c6029
commit
6e2e585175
|
|
@ -74,9 +74,10 @@ type Config struct {
|
||||||
ShowNavigation bool
|
ShowNavigation bool
|
||||||
ShowNewVersions bool
|
ShowNewVersions bool
|
||||||
AuthJwtCookieName string
|
AuthJwtCookieName string
|
||||||
AuthJwtSecret string
|
AuthJwtSecret string // mutually exclusive with pub key config fields
|
||||||
AuthJwtClaimUsername string
|
AuthJwtClaimUsername string
|
||||||
AuthJwtClaimUserGroup string
|
AuthJwtClaimUserGroup string
|
||||||
|
AuthJwtPubKeyPath string // will read pub key from file on disk
|
||||||
AuthHttpHeaderUsername string
|
AuthHttpHeaderUsername string
|
||||||
AuthHttpHeaderUserGroup string
|
AuthHttpHeaderUserGroup string
|
||||||
DefaultPermissions PermissionsList
|
DefaultPermissions PermissionsList
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,10 @@ func parseRequestMetadata(ctx context.Context, req *http.Request) metadata.MD {
|
||||||
return md
|
return md
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetGlobalRestConfig(config *config.Config) {
|
||||||
|
cfg = config
|
||||||
|
}
|
||||||
|
|
||||||
func startRestAPIServer(globalConfig *config.Config) error {
|
func startRestAPIServer(globalConfig *config.Config) error {
|
||||||
cfg = globalConfig
|
cfg = globalConfig
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,45 @@
|
||||||
package httpservers
|
package httpservers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/golang-jwt/jwt/v4"
|
"github.com/golang-jwt/jwt/v4"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
pubKeyBytes []byte = nil
|
||||||
|
pubKey *rsa.PublicKey
|
||||||
)
|
)
|
||||||
|
|
||||||
func parseJwtToken(cookieValue string) (*jwt.Token, error) {
|
func parseJwtToken(cookieValue string) (*jwt.Token, error) {
|
||||||
|
if cfg.AuthJwtPubKeyPath != "" { // activate this path only if pub key is specified
|
||||||
|
if pubKeyBytes == nil { // keep in memory after first load
|
||||||
|
var err error
|
||||||
|
pubKeyBytes, err = os.ReadFile(cfg.AuthJwtPubKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("couldn't read public key from file %s", cfg.AuthJwtPubKeyPath)
|
||||||
|
}
|
||||||
|
// Since the token is RSA (which we validated at the start of this function), the return type of this function actually has to be rsa.PublicKey!
|
||||||
|
pubKey, err = jwt.ParseRSAPublicKeyFromPEM(pubKeyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing public key object (from %s)", cfg.AuthJwtPubKeyPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jwt.Parse(cookieValue, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"expected token algorithm '%v' but got '%v'",
|
||||||
|
jwt.SigningMethodRS256.Name,
|
||||||
|
token.Header)
|
||||||
|
}
|
||||||
|
return pubKey, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return jwt.Parse(cookieValue, func(token *jwt.Token) (interface{}, error) {
|
return jwt.Parse(cookieValue, func(token *jwt.Token) (interface{}, error) {
|
||||||
// Don't forget to validate the alg is what you expect:
|
// Don't forget to validate the alg is what you expect:
|
||||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
package httpservers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
config2 "github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/cors"
|
||||||
|
"github.com/golang-jwt/jwt/v4"
|
||||||
|
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testBase(t *testing.T, expire int64, expectCode int) {
|
||||||
|
tmpFile, _ := os.CreateTemp(os.TempDir(), "olivetin-jwt-")
|
||||||
|
defer os.Remove(tmpFile.Name())
|
||||||
|
|
||||||
|
fmt.Println("Created File: " + tmpFile.Name())
|
||||||
|
|
||||||
|
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
pubKey := &privateKey.PublicKey
|
||||||
|
// https://stackoverflow.com/questions/13555085/save-and-load-crypto-rsa-privatekey-to-and-from-the-disk
|
||||||
|
pkixPubKey, _ := x509.MarshalPKIXPublicKey(pubKey)
|
||||||
|
pubPem := pem.EncodeToMemory(
|
||||||
|
&pem.Block{
|
||||||
|
Type: "RSA PUBLIC KEY",
|
||||||
|
Bytes: pkixPubKey,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err := os.WriteFile(tmpFile.Name(), pubPem, 0755); err != nil {
|
||||||
|
fmt.Printf("error when dumping pubKey: %s \n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// default config + overrides
|
||||||
|
config := config2.DefaultConfig()
|
||||||
|
config.AuthJwtPubKeyPath = tmpFile.Name()
|
||||||
|
config.AuthJwtClaimUsername = "sub"
|
||||||
|
config.AuthJwtClaimUserGroup = "olivetinGroup"
|
||||||
|
config.AuthJwtCookieName = "authorization_token"
|
||||||
|
SetGlobalRestConfig(config) // ugly, setting global var, we should pass configs as params to modules... :/
|
||||||
|
|
||||||
|
token := jwt.New(jwt.SigningMethodRS256)
|
||||||
|
|
||||||
|
claims := token.Claims.(jwt.MapClaims)
|
||||||
|
claims["nbf"] = time.Now().Unix() - 1000
|
||||||
|
claims["exp"] = time.Now().Unix() + expire
|
||||||
|
claims["sub"] = "test"
|
||||||
|
claims["olivetinGroup"] = "test"
|
||||||
|
|
||||||
|
tokenStr, _ := token.SignedString(privateKey)
|
||||||
|
|
||||||
|
// init mux endpoint like in restapi.go (but using dummy response handler)
|
||||||
|
mux := runtime.NewServeMux(
|
||||||
|
runtime.WithMetadata(parseRequestMetadata), // i am guessing this is critical middleware for authorizing request cookie
|
||||||
|
runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{
|
||||||
|
Marshaler: &runtime.JSONPb{
|
||||||
|
MarshalOptions: protojson.MarshalOptions{
|
||||||
|
UseProtoNames: true,
|
||||||
|
EmitUnpopulated: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
mux.HandlePath("GET", "/", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
|
||||||
|
username, usergroup := parseJwtCookie(r)
|
||||||
|
if username == "" {
|
||||||
|
w.WriteHeader(403)
|
||||||
|
}
|
||||||
|
w.Write([]byte(fmt.Sprintf("username=%v, usergroup=%v", username, usergroup)))
|
||||||
|
})
|
||||||
|
|
||||||
|
// make server and attach handler
|
||||||
|
srv := &http.Server{Handler: cors.AllowCors(mux)}
|
||||||
|
lis, _ := net.Listen("tcp", ":1337")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := srv.Serve(lis); err != nil {
|
||||||
|
t.Errorf("couldn't start server: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// make http client and send request to myself
|
||||||
|
client := &http.Client{}
|
||||||
|
req, _ := http.NewRequest("GET", "http://localhost:1337/", nil)
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: "authorization_token",
|
||||||
|
Value: tokenStr,
|
||||||
|
MaxAge: 300,
|
||||||
|
}
|
||||||
|
req.AddCookie(cookie)
|
||||||
|
res, _ := client.Do(req)
|
||||||
|
defer res.Body.Close()
|
||||||
|
assert.Equal(t, expectCode, res.StatusCode)
|
||||||
|
body, _ := io.ReadAll(res.Body)
|
||||||
|
fmt.Println(string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJWTSignatureVerificationSucceeds(t *testing.T) {
|
||||||
|
testBase(t, 1000, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJWTSignatureVerificationFails(t *testing.T) {
|
||||||
|
testBase(t, -500, 403)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue