Commit 2bbe836a authored by Sietse Ringers's avatar Sietse Ringers
Browse files

Add support for symmetric HMAC JWTs with HS256 signature algorithm


Co-authored-by: Tomas's avatarConfiks <confiks@scriptbase.org>
parent b3c96041
package irmaserver
import (
"crypto/rsa"
"encoding/base64"
"encoding/json"
"io/ioutil"
"net/http"
"os"
......@@ -36,13 +37,17 @@ type AuthenticationMethod string
// Currently supported requestor authentication methods
const (
AuthenticationMethodHmac = "hmac"
AuthenticationMethodPublicKey = "publickey"
AuthenticationMethodToken = "token"
AuthenticationMethodNone = "none"
)
type HmacAuthenticator struct {
hmackeys map[string]interface{}
}
type PublicKeyAuthenticator struct {
publickeys map[string]*rsa.PublicKey
publickeys map[string]interface{}
}
type PresharedKeyAuthenticator struct {
presharedkeys map[string]string
......@@ -68,34 +73,28 @@ func (NilAuthenticator) Initialize(name string, requestor Requestor) error {
return nil
}
func (pkauth *PublicKeyAuthenticator) Authenticate(
func (hauth *HmacAuthenticator) Authenticate(
headers http.Header, body []byte,
) (bool, irma.SessionRequest, string, *irma.RemoteError) {
if headers.Get("Authorization") != "" || !strings.HasPrefix(headers.Get("Content-Type"), "text/plain") {
return false, nil, "", nil
}
) (applies bool, request irma.SessionRequest, requestor string, err *irma.RemoteError) {
return jwtAuthenticate(headers, body, jwt.SigningMethodHS256.Name, hauth.hmackeys)
}
requestorJwt := string(body)
claims := &jwt.StandardClaims{}
token, err := jwt.ParseWithClaims(requestorJwt, claims, jwtPublicKeyExtractor(pkauth.publickeys))
if err != nil {
return true, nil, "", server.RemoteError(server.ErrorInvalidRequest, err.Error())
}
if !claims.VerifyIssuedAt(time.Now().Unix(), true) {
return true, nil, "", server.RemoteError(server.ErrorUnauthorized, "jwt not yet valid")
}
if time.Unix(claims.IssuedAt, 0).Add(10 * time.Minute).Before(time.Now()) { // TODO make configurable
return true, nil, "", server.RemoteError(server.ErrorUnauthorized, "jwt too old")
func (hauth *HmacAuthenticator) Initialize(name string, requestor Requestor) error {
if requestor.AuthenticationKey == "" {
return errors.Errorf("Requestor %s had no authentication key")
}
// Read JWT contents
parsedJwt, err := irma.ParseRequestorJwt(claims.Subject, requestorJwt)
if err != nil {
return true, nil, "", server.RemoteError(server.ErrorInvalidRequest, err.Error())
var bts []byte
if _, err := base64.StdEncoding.Decode(bts, []byte(requestor.AuthenticationKey)); err != nil {
return err
}
hauth.hmackeys[name] = bts
return nil
}
requestor := token.Header["kid"].(string) // presence in Header and type is already checked by jwtPublicKeyExtractor
return true, parsedJwt.SessionRequest(), requestor, nil
func (pkauth *PublicKeyAuthenticator) Authenticate(
headers http.Header, body []byte,
) (bool, irma.SessionRequest, string, *irma.RemoteError) {
return jwtAuthenticate(headers, body, jwt.SigningMethodRS256.Name, pkauth.publickeys)
}
func (pkauth *PublicKeyAuthenticator) Initialize(name string, requestor Requestor) error {
......@@ -150,8 +149,8 @@ func (pskauth *PresharedKeyAuthenticator) Initialize(name string, requestor Requ
// Helper functions
// Given an (unauthenticated) jwt, return the public key against which it should be verified using the "kid" header
func jwtPublicKeyExtractor(publickeys map[string]*rsa.PublicKey) func(token *jwt.Token) (interface{}, error) {
// Given an (unauthenticated) jwt, return the key against which it should be verified using the "kid" header
func jwtKeyExtractor(publickeys map[string]interface{}) func(token *jwt.Token) (interface{}, error) {
return func(token *jwt.Token) (interface{}, error) {
var ok bool
kid, ok := token.Header["kid"]
......@@ -168,3 +167,71 @@ func jwtPublicKeyExtractor(publickeys map[string]*rsa.PublicKey) func(token *jwt
return nil, errors.Errorf("Unknown requestor: %s", requestor)
}
}
// jwtAuthenticate is a helper function for JWT-based authenticators that verifies and parses JWTs.
func jwtAuthenticate(
headers http.Header, body []byte, signatureAlg string, keys map[string]interface{},
) (bool, irma.SessionRequest, string, *irma.RemoteError) {
// Read JWT and check its type
if headers.Get("Authorization") != "" || !strings.HasPrefix(headers.Get("Content-Type"), "text/plain") {
return false, nil, "", nil
}
requestorJwt := string(body)
alg, err := jwtSignatureAlg(requestorJwt)
if err != nil || alg != signatureAlg {
// If err != nil, ie. we failed to determine the JWT signature algorithm, we assume that the
// request is not meant for this authenticator. So we don't return err
return false, nil, "", nil
}
// Verify JWT signature
claims := &jwt.StandardClaims{}
token, err := jwt.ParseWithClaims(requestorJwt, claims, jwtKeyExtractor(keys))
if err != nil {
return true, nil, "", server.RemoteError(server.ErrorInvalidRequest, err.Error())
}
if !claims.VerifyIssuedAt(time.Now().Unix(), true) {
return true, nil, "", server.RemoteError(server.ErrorUnauthorized, "jwt not yet valid")
}
if time.Unix(claims.IssuedAt, 0).Add(10 * time.Minute).Before(time.Now()) { // TODO make configurable
return true, nil, "", server.RemoteError(server.ErrorUnauthorized, "jwt too old")
}
// Read JWT contents
parsedJwt, err := irma.ParseRequestorJwt(claims.Subject, requestorJwt)
if err != nil {
return true, nil, "", server.RemoteError(server.ErrorInvalidRequest, err.Error())
}
requestor := token.Header["kid"].(string) // presence in Header and type is already checked by jwtKeyExtractor
return true, parsedJwt.SessionRequest(), requestor, nil
}
func jwtSignatureAlg(j string) (string, error) {
var (
alg string
header map[string]interface{}
i interface{}
ok bool
bts []byte
err error
)
segments := strings.Split(j, ".")
if len(segments) == 0 {
return "", errors.New("invalid jwt, not enough segments")
}
if bts, err = jwt.DecodeSegment(segments[0]); err != nil {
return "", errors.WrapPrefix(err, "failed to base64-decode jwt header", 0)
}
if err := json.Unmarshal(bts, &header); err != nil {
return "", errors.WrapPrefix(err, "failed to json-deserialize jwt header", 0)
}
if i, ok = header["alg"]; !ok {
return "", errors.New("alg field not found in jwt header")
}
if alg, ok = i.(string); !ok {
return "", errors.New("alg field in jwt was not a string")
}
return alg, nil
}
......@@ -137,7 +137,8 @@ func (conf *Configuration) initialize() error {
authenticators = map[AuthenticationMethod]Authenticator{AuthenticationMethodNone: NilAuthenticator{}}
} else {
authenticators = map[AuthenticationMethod]Authenticator{
AuthenticationMethodPublicKey: &PublicKeyAuthenticator{publickeys: map[string]*rsa.PublicKey{}},
AuthenticationMethodHmac: &HmacAuthenticator{hmackeys: map[string]interface{}{}},
AuthenticationMethodPublicKey: &PublicKeyAuthenticator{publickeys: map[string]interface{}{}},
AuthenticationMethodToken: &PresharedKeyAuthenticator{presharedkeys: map[string]string{}},
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment