Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add kid header #184

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ _Note for Caddy users_: Not all parameters are available in Caddy. See the table
| -jwt-secret | string | "random key" | X | Secret used to sign the JWT token. (See [caddy/README.md](./caddy/README.md) for details.) |
| -jwt-secret-file | string | | X | File to load the jwt-secret from, e.g. `/run/secrets/some.key`. **Takes precedence over jwt-secret!** |
| -jwt-algo | string | "HS512" | X | Signing algorithm to use (ES256, ES384, ES512, RS256, RS384, RS512, HS256, HS384, HS512) |
| -jwt-key-id | string | | X | The key id to use, added to the jwt header as kid if set |
| -log-level | string | "info" | - | Log level |
| -login-path | string | "/login" | X | Path of the login resource |
| -logout-url | string | | X | URL or path to redirect to after logout |
Expand Down
8 changes: 4 additions & 4 deletions caddy/handler.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package caddy

import (
"context"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
"github.com/tarent/loginsrv/login"
"context"
"net/http"
"strings"
)
Expand All @@ -27,13 +27,13 @@ func NewCaddyHandler(next httpserver.Handler, loginHandler *login.Handler, confi

func (h *CaddyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
//Fetch jwt token. If valid set a Caddy replacer for {user}
userInfo, valid := h.loginHandler.GetToken(r)
userInfo, _, valid := h.loginHandler.GetToken(r)
if valid {
// let upstream middleware (e.g. fastcgi and cgi) know about authenticated
// user; this replaces the request with a wrapped instance
r = r.WithContext(context.WithValue(r.Context(),
httpserver.RemoteUserCtxKey, userInfo.Sub))
httpserver.RemoteUserCtxKey, userInfo.Sub))

// Provide username to be used in log by replacer
repl := httpserver.NewReplacer(r, nil, "-")
repl.Set("user", userInfo.Sub)
Expand Down
4 changes: 2 additions & 2 deletions caddy/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import (
"testing"
"time"

"github.com/dgrijalva/jwt-go"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
"github.com/dgrijalva/jwt-go"
"github.com/tarent/loginsrv/login"
"github.com/tarent/loginsrv/model"
)
Expand Down Expand Up @@ -63,7 +63,7 @@ func Test_ServeHTTP_200(t *testing.T) {
r.AddCookie(&cookie)

//Test that cookie is a valid token
_, valid := loginh.GetToken(r)
_, _, valid := loginh.GetToken(r)
if !valid {
t.Errorf("loginHandler cookie is not valid")
}
Expand Down
5 changes: 4 additions & 1 deletion login/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func DefaultConfig() *Config {
JwtAlgo: "HS512",
JwtExpiry: 24 * time.Hour,
JwtRefreshes: 0,
JwtKeyID: "",
SuccessURL: "/",
Redirect: true,
RedirectQueryParameter: "backTo",
Expand Down Expand Up @@ -68,6 +69,7 @@ type Config struct {
JwtAlgo string
JwtExpiry time.Duration
JwtRefreshes int
JwtKeyID string
SuccessURL string
Redirect bool
RedirectQueryParameter string
Expand Down Expand Up @@ -139,9 +141,10 @@ func (c *Config) ConfigureFlagSet(f *flag.FlagSet) {
f.BoolVar(&c.TextLogging, "text-logging", c.TextLogging, "Log in text format instead of json")
f.StringVar(&c.JwtSecret, "jwt-secret", c.JwtSecret, "The secret to sign the jwt token")
f.StringVar(&c.JwtSecretFile, "jwt-secret-file", c.JwtSecretFile, "Path to a file containing the secret to sign the jwt token (overrides jwt-secret)")
f.StringVar(&c.JwtAlgo, "jwt-algo", c.JwtAlgo, "The singing algorithm to use (ES256, ES384, ES512, RS256, RS384, RS512, HS256, HS384, HS512")
f.StringVar(&c.JwtAlgo, "jwt-algo", c.JwtAlgo, "The signing algorithm to use (ES256, ES384, ES512, RS256, RS384, RS512, HS256, HS384, HS512)")
f.DurationVar(&c.JwtExpiry, "jwt-expiry", c.JwtExpiry, "The expiry duration for the jwt token, e.g. 2h or 3h30m")
f.IntVar(&c.JwtRefreshes, "jwt-refreshes", c.JwtRefreshes, "The maximum amount of jwt refreshes. 0 by Default")
f.StringVar(&c.JwtKeyID, "jwt-key-id", c.JwtKeyID, "The key id to use, added to the jwt header as kid if set")
f.StringVar(&c.CookieName, "cookie-name", c.CookieName, "The name of the jwt cookie")
f.BoolVar(&c.CookieHTTPOnly, "cookie-http-only", c.CookieHTTPOnly, "Set the cookie with the http only flag")
f.BoolVar(&c.CookieSecure, "cookie-secure", c.CookieSecure, "Set the cookie with the secure flag")
Expand Down
4 changes: 4 additions & 0 deletions login/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func TestConfig_ReadConfig(t *testing.T) {
"--jwt-secret=jwtsecret",
"--jwt-algo=algo",
"--jwt-expiry=42h42m",
"--jwt-key-id=id",
"--success-url=successurl",
"--redirect=false",
"--redirect-query-parameter=comingFrom",
Expand Down Expand Up @@ -62,6 +63,7 @@ func TestConfig_ReadConfig(t *testing.T) {
JwtSecret: "jwtsecret",
JwtAlgo: "algo",
JwtExpiry: 42*time.Hour + 42*time.Minute,
JwtKeyID: "id",
SuccessURL: "successurl",
Redirect: false,
RedirectQueryParameter: "comingFrom",
Expand Down Expand Up @@ -159,6 +161,7 @@ func TestConfig_ReadConfigFromEnv(t *testing.T) {
NoError(t, os.Setenv("LOGINSRV_JWT_SECRET", "jwtsecret"))
NoError(t, os.Setenv("LOGINSRV_JWT_ALGO", "algo"))
NoError(t, os.Setenv("LOGINSRV_JWT_EXPIRY", "42h42m"))
NoError(t, os.Setenv("LOGINSRV_JWT_KEY_ID", "id"))
NoError(t, os.Setenv("LOGINSRV_SUCCESS_URL", "successurl"))
NoError(t, os.Setenv("LOGINSRV_REDIRECT", "false"))
NoError(t, os.Setenv("LOGINSRV_REDIRECT_QUERY_PARAMETER", "comingFrom"))
Expand Down Expand Up @@ -188,6 +191,7 @@ func TestConfig_ReadConfigFromEnv(t *testing.T) {
JwtSecret: "jwtsecret",
JwtAlgo: "algo",
JwtExpiry: 42*time.Hour + 42*time.Minute,
JwtKeyID: "id",
SuccessURL: "successurl",
Redirect: false,
RedirectQueryParameter: "comingFrom",
Expand Down
18 changes: 11 additions & 7 deletions login/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) {
}

if r.Method == "GET" {
userInfo, valid := h.GetToken(r)
userInfo, _, valid := h.GetToken(r)
if wantJSON(r) {
if valid {
w.Header().Set("Content-Type", contentTypeJSON)
Expand Down Expand Up @@ -176,7 +176,7 @@ func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) {
h.handleAuthentication(w, r, username, password)
return
}
userInfo, valid := h.GetToken(r)
userInfo, _, valid := h.GetToken(r)
if valid {
h.handleRefresh(w, r, userInfo)
return
Expand Down Expand Up @@ -289,29 +289,33 @@ func (h *Handler) createToken(userInfo model.UserInfo) (string, error) {
return "", err
}
token := jwt.NewWithClaims(signingMethod, claims)
if h.config.JwtKeyID != "" {
token.Header["kid"] = h.config.JwtKeyID
}

return token.SignedString(key)
}

func (h *Handler) GetToken(r *http.Request) (userInfo model.UserInfo, valid bool) {
func (h *Handler) GetToken(r *http.Request) (userInfo model.UserInfo, headers map[string]interface{}, valid bool) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't introduce return values for testing purposes.

c, err := r.Cookie(h.config.CookieName)
if err != nil {
return model.UserInfo{}, false
return model.UserInfo{}, nil, false
}

token, err := jwt.ParseWithClaims(c.Value, &model.UserInfo{}, func(*jwt.Token) (interface{}, error) {
_, _, verifyKey, err := h.signingInfo()
return verifyKey, err
})
if err != nil {
return model.UserInfo{}, false
return model.UserInfo{}, nil, false
}

u, ok := token.Claims.(*model.UserInfo)
if !ok {
return model.UserInfo{}, false
return model.UserInfo{}, nil, false
}

return *u, u.Valid() == nil
return *u, token.Header, u.Valid() == nil
}

func (h *Handler) signingInfo() (signingMethod jwt.SigningMethod, key, verifyKey interface{}, err error) {
Expand Down
35 changes: 27 additions & 8 deletions login/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -455,11 +455,30 @@ func TestHandler_getToken_Valid(t *testing.T) {
r := &http.Request{
Header: http.Header{"Cookie": {h.config.CookieName + "=" + token + ";"}},
}
userInfo, valid := h.GetToken(r)
userInfo, _, valid := h.GetToken(r)
True(t, valid)
Equal(t, input, userInfo)
}

func TestHandler_getToken_Key_ID(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Name of the Test does not describe the tested feature. It should state that the kid value is stored in the jwt header if a token is generated.

h := testHandler()
h.config.JwtKeyID = "some-id"
input := model.UserInfo{Sub: "marvin", Expiry: time.Now().Add(time.Second).Unix()}
token, err := h.createToken(input)
NoError(t, err)
r := &http.Request{
Header: http.Header{"Cookie": {h.config.CookieName + "=" + token + ";"}},
}
userInfo, headers, valid := h.GetToken(r)
True(t, valid)
Equal(t, input, userInfo)
Equal(t, map[string]interface{}{
"alg": "HS512",
"kid": "some-id",
"typ": "JWT",
}, headers)
}

func TestHandler_ReturnUserInfoJSON(t *testing.T) {
h := testHandler()
input := model.UserInfo{Sub: "marvin", Expiry: time.Now().Add(time.Second).Unix()}
Expand Down Expand Up @@ -515,7 +534,7 @@ func TestHandler_signAndVerify_ES256(t *testing.T) {
r := &http.Request{
Header: http.Header{"Cookie": {h.config.CookieName + "=" + token + ";"}},
}
userInfo, valid := h.GetToken(r)
userInfo, _, valid := h.GetToken(r)
True(t, valid)
Equal(t, input, userInfo)
}
Expand Down Expand Up @@ -549,7 +568,7 @@ func TestHandler_signAndVerify_RSA(t *testing.T) {
r := &http.Request{
Header: http.Header{"Cookie": {h.config.CookieName + "=" + token + ";"}},
}
userInfo, valid := h.GetToken(r)
userInfo, _, valid := h.GetToken(r)
True(t, valid)
Equal(t, input, userInfo)
})
Expand Down Expand Up @@ -579,7 +598,7 @@ func TestHandler_signAndVerify_RSA(t *testing.T) {
r := &http.Request{
Header: http.Header{"Cookie": {h.config.CookieName + "=" + token + ";"}},
}
userInfo, valid := h.GetToken(r)
userInfo, _, valid := h.GetToken(r)
True(t, valid)
Equal(t, input, userInfo)
})
Expand All @@ -605,7 +624,7 @@ func TestHandler_getToken_InvalidSecret(t *testing.T) {
}
// modify secret
h.config.JwtSecret = "foobar"
_, valid := h.GetToken(r)
_, _, valid := h.GetToken(r)
False(t, valid)
}

Expand All @@ -615,13 +634,13 @@ func TestHandler_getToken_InvalidToken(t *testing.T) {
Header: http.Header{"Cookie": {h.config.CookieName + "=asdcsadcsadc"}},
}

_, valid := h.GetToken(r)
_, _, valid := h.GetToken(r)
False(t, valid)
}

func TestHandler_getToken_InvalidNoToken(t *testing.T) {
h := testHandler()
_, valid := h.GetToken(&http.Request{})
_, _, valid := h.GetToken(&http.Request{})
False(t, valid)
}

Expand All @@ -637,7 +656,7 @@ func TestHandler_getToken_WithUserClaims(t *testing.T) {
r := &http.Request{
Header: http.Header{"Cookie": {h.config.CookieName + "=" + token + ";"}},
}
userInfo, valid := h.GetToken(r)
userInfo, _, valid := h.GetToken(r)
True(t, valid)
Equal(t, "Zappod", userInfo.Sub)
Equal(t, "fake", userInfo.Origin)
Expand Down