extract full claims from jwt token to get session id

Signed-off-by: Christian Richter <crichter@owncloud.com>
This commit is contained in:
Christian Richter
2023-04-11 15:49:24 +02:00
parent e543c8f60d
commit a3640b0565
6 changed files with 655 additions and 22 deletions

2
go.mod
View File

@@ -100,6 +100,7 @@ require (
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f
google.golang.org/grpc v1.54.0
google.golang.org/protobuf v1.28.1
gopkg.in/square/go-jose.v2 v2.6.0
gopkg.in/yaml.v2 v2.4.0
gotest.tools/v3 v3.4.0
stash.kopano.io/kgol/oidc-go v0.3.4
@@ -313,7 +314,6 @@ require (
golang.org/x/tools v0.7.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

2
go.sum
View File

@@ -631,6 +631,8 @@ github.com/cs3org/reva/v2 v2.12.1-0.20230404090709-bb973fae26ae h1:APfYubzIYqCTX
github.com/cs3org/reva/v2 v2.12.1-0.20230404090709-bb973fae26ae/go.mod h1:FNAYs5H3xs8v0OFmNgZtiMAzIMXd/6TJmO0uZuNn8pQ=
github.com/cs3org/reva/v2 v2.12.1-0.20230417084429-b3d96f9db80c h1:H6OjKTaRowZfAU/Hwvv4W0pLFFH/KNbHaNVNw3ANoHU=
github.com/cs3org/reva/v2 v2.12.1-0.20230417084429-b3d96f9db80c/go.mod h1:FNAYs5H3xs8v0OFmNgZtiMAzIMXd/6TJmO0uZuNn8pQ=
github.com/cs3org/reva/v2 v2.12.1-0.20230331184913-eca8953fb6e9 h1:zUMD0UvKVPbR3UaodnA0GXuBycxrIuaO8Kshgi3iKKI=
github.com/cs3org/reva/v2 v2.12.1-0.20230331184913-eca8953fb6e9/go.mod h1:FNAYs5H3xs8v0OFmNgZtiMAzIMXd/6TJmO0uZuNn8pQ=
github.com/cubewise-code/go-mime v0.0.0-20200519001935-8c5762b177d8 h1:Z9lwXumT5ACSmJ7WGnFl+OMLLjpz5uR2fyz7dC255FI=
github.com/cubewise-code/go-mime v0.0.0-20200519001935-8c5762b177d8/go.mod h1:4abs/jPXcmJzYoYGF91JF9Uq9s/KL5n1jvFDix8KcqY=
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=

View File

@@ -0,0 +1,238 @@
package oidc
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"gopkg.in/square/go-jose.v2"
"time"
gOidc "github.com/coreos/go-oidc/v3/oidc"
)
// This adds the ability to verify Logout Tokens as specified in https://openid.net/specs/openid-connect-backchannel-1_0.html
type logoutEvent struct {
Event *struct{} `json:"http://schemas.openid.net/event/backchannel-logout"`
}
type audience []string
func (a *audience) UnmarshalJSON(b []byte) error {
var s string
if json.Unmarshal(b, &s) == nil {
*a = audience{s}
return nil
}
var auds []string
if err := json.Unmarshal(b, &auds); err != nil {
return err
}
*a = auds
return nil
}
type jsonTime time.Time
func (j *jsonTime) UnmarshalJSON(b []byte) error {
var n json.Number
if err := json.Unmarshal(b, &n); err != nil {
return err
}
var unix int64
if t, err := n.Int64(); err == nil {
unix = t
} else {
f, err := n.Float64()
if err != nil {
return err
}
unix = int64(f)
}
*j = jsonTime(time.Unix(unix, 0))
return nil
}
// logoutToken
type logoutToken struct {
Issuer string `json:"iss"`
Subject string `json:"sub"`
Audience audience `json:"aud"`
IssuedAt jsonTime `json:"iat"`
JwtID string `json:"jti"`
Events logoutEvent `json:"events"`
Sid string `json:"sid"`
}
// Logout Token
type LogoutToken struct {
// The URL of the server which issued this token. OpenID Connect
// requires this value always be identical to the URL used for
// initial discovery.
//
// Note: Because of a known issue with Google Accounts' implementation
// this value may differ when using Google.
//
// See: https://developers.google.com/identity/protocols/OpenIDConnect#obtainuserinfo
Issuer string
// A unique string which identifies the end user.
Subject string
// The client ID, or set of client IDs, that this token is issued for. For
// common uses, this is the client that initialized the auth flow.
//
// This package ensures the audience contains an expected value.
Audience []string
// When the token was issued by the provider.
IssuedAt time.Time
// The Session Id
SessionId string
// Jwt Id
JwtID string
}
// LogoutTokenVerifier provides verification for Logout Tokens.
type LogoutTokenVerifier struct {
keySet gOidc.KeySet
config *gOidc.Config
issuer string
}
func NewLogoutVerifier(issuerURL string, keySet gOidc.KeySet, config *gOidc.Config) *LogoutTokenVerifier {
return &LogoutTokenVerifier{keySet: keySet, config: config, issuer: issuerURL}
}
//Upon receiving a logout request at the back-channel logout URI, the RP MUST validate the Logout Token as follows:
//
//1. If the Logout Token is encrypted, decrypt it using the keys and algorithms that the Client specified during Registration that the OP was to use to encrypt ID Tokens. If ID Token encryption was negotiated with the OP at Registration time and the Logout Token is not encrypted, the RP SHOULD reject it.
//2. Validate the Logout Token signature in the same way that an ID Token signature is validated, with the following refinements.
//3. Validate the iss, aud, and iat Claims in the same way they are validated in ID Tokens.
//4. Verify that the Logout Token contains a sub Claim, a sid Claim, or both.
//5. Verify that the Logout Token contains an events Claim whose value is JSON object containing the member name http://schemas.openid.net/event/backchannel-logout.
//6. Verify that the Logout Token does not contain a nonce Claim.
//7. Optionally verify that another Logout Token with the same jti value has not been recently received.
//If any of the validation steps fails, reject the Logout Token and return an HTTP 400 Bad Request error. Otherwise, proceed to perform the logout actions.
// Verify verifies a Logout token according to Specs
func (v *LogoutTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*LogoutToken, error) {
jws, err := jose.ParseSigned(rawIDToken)
if err != nil {
return nil, err
}
// Throw out tokens with invalid claims before trying to verify the token. This lets
// us do cheap checks before possibly re-syncing keys.
payload, err := parseJWT(rawIDToken)
if err != nil {
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
}
var token logoutToken
if err := json.Unmarshal(payload, &token); err != nil {
return nil, fmt.Errorf("oidc: failed to unmarshal claims: %v", err)
}
//4. Verify that the Logout Token contains a sub Claim, a sid Claim, or both.
if token.Subject == "" && token.Sid == "" {
return nil, fmt.Errorf("oidc: logout token must contain either sub or sid and MAY contain both")
}
//5. Verify that the Logout Token contains an events Claim whose value is JSON object containing the member name http://schemas.openid.net/event/backchannel-logout.
if token.Events.Event == nil {
return nil, fmt.Errorf("oidc: logout token must contain logout event")
}
//6. Verify that the Logout Token does not contain a nonce Claim.
type nonce struct {
Nonce *string `json:"nonce"`
}
var n nonce
json.Unmarshal(payload, &n)
if n.Nonce != nil {
return nil, fmt.Errorf("oidc: nonce on logout token MUST NOT be present")
}
// Check issuer.
if !v.config.SkipIssuerCheck && token.Issuer != v.issuer {
return nil, fmt.Errorf("oidc: id token issued by a different provider, expected %q got %q", v.issuer, token.Issuer)
}
// If a client ID has been provided, make sure it's part of the audience. SkipClientIDCheck must be true if ClientID is empty.
//
// This check DOES NOT ensure that the ClientID is the party to which the ID Token was issued (i.e. Authorized party).
if !v.config.SkipClientIDCheck {
if v.config.ClientID != "" {
if !contains(token.Audience, v.config.ClientID) {
return nil, fmt.Errorf("oidc: expected audience %q got %q", v.config.ClientID, token.Audience)
}
} else {
return nil, fmt.Errorf("oidc: invalid configuration, clientID must be provided or SkipClientIDCheck must be set")
}
}
switch len(jws.Signatures) {
case 0:
return nil, fmt.Errorf("oidc: id token not signed")
case 1:
default:
return nil, fmt.Errorf("oidc: multiple signatures on id token not supported")
}
sig := jws.Signatures[0]
supportedSigAlgs := v.config.SupportedSigningAlgs
if len(supportedSigAlgs) == 0 {
supportedSigAlgs = []string{gOidc.RS256}
}
if !contains(supportedSigAlgs, sig.Header.Algorithm) {
return nil, fmt.Errorf("oidc: id token signed with unsupported algorithm, expected %q got %q", supportedSigAlgs, sig.Header.Algorithm)
}
gotPayload, err := v.keySet.VerifySignature(ctx, rawIDToken)
if err != nil {
return nil, fmt.Errorf("failed to verify signature: %v", err)
}
// Ensure that the payload returned by the square actually matches the payload parsed earlier.
if !bytes.Equal(gotPayload, payload) {
return nil, errors.New("oidc: internal error, payload parsed did not match previous payload")
}
t := &LogoutToken{
Issuer: token.Issuer,
Subject: token.Subject,
Audience: token.Audience,
IssuedAt: time.Time(token.IssuedAt),
SessionId: token.Sid,
JwtID: token.JwtID,
}
return t, nil
}
func parseJWT(p string) ([]byte, error) {
parts := strings.Split(p, ".")
if len(parts) < 2 {
return nil, fmt.Errorf("oidc: malformed jwt, expected 3 parts got %d", len(parts))
}
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, fmt.Errorf("oidc: malformed jwt payload: %v", err)
}
return payload, nil
}
func contains(sli []string, ele string) bool {
for _, s := range sli {
if s == ele {
return true
}
}
return false
}

View File

@@ -0,0 +1,296 @@
package oidc
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"fmt"
"testing"
gOidc "github.com/coreos/go-oidc/v3/oidc"
"gopkg.in/square/go-jose.v2"
)
type signingKey struct {
keyID string // optional
priv interface{}
pub interface{}
alg jose.SignatureAlgorithm
}
// sign creates a JWS using the private key from the provided payload.
func (s *signingKey) sign(t testing.TB, payload []byte) string {
privKey := &jose.JSONWebKey{Key: s.priv, Algorithm: string(s.alg), KeyID: s.keyID}
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: s.alg, Key: privKey}, nil)
if err != nil {
t.Fatal(err)
}
jws, err := signer.Sign(payload)
if err != nil {
t.Fatal(err)
}
data, err := jws.CompactSerialize()
if err != nil {
t.Fatal(err)
}
return data
}
func (s *signingKey) jwk() jose.JSONWebKey {
return jose.JSONWebKey{Key: s.pub, Use: "sig", Algorithm: string(s.alg), KeyID: s.keyID}
}
func TestLogoutVerify(t *testing.T) {
tests := []logoutVerificationTest{
{
name: "good token",
logoutToken: ` {
"iss": "https://foo",
"sub": "248289761001",
"aud": "s6BhdRkqt3",
"iat": 1471566154,
"jti": "bWJq",
"sid": "08a5019c-17e1-4977-8f42-65a12843ea02",
"events": {
"http://schemas.openid.net/event/backchannel-logout": {}
}
}`,
config: gOidc.Config{
SkipClientIDCheck: true,
},
signKey: newRSAKey(t),
},
{
name: "invalid issuer",
issuer: "https://bar",
logoutToken: `{"iss":"https://foo"}`,
config: gOidc.Config{
SkipClientIDCheck: true,
SkipExpiryCheck: true,
},
signKey: newRSAKey(t),
wantErr: true,
},
{
name: "invalid sig",
logoutToken: `{
"iss": "https://foo",
"sub": "248289761001",
"aud": "s6BhdRkqt3",
"iat": 1471566154,
"jti": "bWJq",
"sid": "08a5019c-17e1-4977-8f42-65a12843ea02",
"events": {
"http://schemas.openid.net/event/backchannel-logout": {}
}
}`,
config: gOidc.Config{
SkipClientIDCheck: true,
SkipExpiryCheck: true,
},
signKey: newRSAKey(t),
verificationKey: newRSAKey(t),
wantErr: true,
},
{
name: "no sid and no sub",
logoutToken: ` {
"iss": "https://foo",
"aud": "s6BhdRkqt3",
"iat": 1471566154,
"jti": "bWJq",
"events": {
"http://schemas.openid.net/event/backchannel-logout": {}
}
}`,
config: gOidc.Config{
SkipClientIDCheck: true,
},
signKey: newRSAKey(t),
wantErr: true,
},
{
name: "Prohibited nonce present",
logoutToken: ` {
"iss": "https://foo",
"sub": "248289761001",
"aud": "s6BhdRkqt3",
"iat": 1471566154,
"jti": "bWJq",
"nonce" : "prohibited",
"events": {
"http://schemas.openid.net/event/backchannel-logout": {}
}
}`,
config: gOidc.Config{
SkipClientIDCheck: true,
},
signKey: newRSAKey(t),
wantErr: true,
},
{
name: "Wrong Event string",
logoutToken: ` {
"iss": "https://foo",
"sub": "248289761001",
"aud": "s6BhdRkqt3",
"iat": 1471566154,
"jti": "bWJq",
"sid": "08a5019c-17e1-4977-8f42-65a12843ea02",
"events": {
"not a logout event": {}
}
}`,
config: gOidc.Config{
SkipClientIDCheck: true,
},
signKey: newRSAKey(t),
wantErr: true,
},
{
name: "No Event string",
logoutToken: ` {
"iss": "https://foo",
"sub": "248289761001",
"aud": "s6BhdRkqt3",
"iat": 1471566154,
"jti": "bWJq",
"sid": "08a5019c-17e1-4977-8f42-65a12843ea02",
}`,
config: gOidc.Config{
SkipClientIDCheck: true,
},
signKey: newRSAKey(t),
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, test.run)
}
}
func TestVerifyAudienceLogout(t *testing.T) {
tests := []logoutVerificationTest{
{
name: "good audience",
logoutToken: `{"iss":"https://foo","aud":"client1","sub":"subject","events": {
"http://schemas.openid.net/event/backchannel-logout": {}
}
}`,
config: gOidc.Config{
ClientID: "client1",
SkipExpiryCheck: true,
},
signKey: newRSAKey(t),
},
{
name: "mismatched audience",
logoutToken: `{"iss":"https://foo","aud":"client2","sub":"subject","events": {
"http://schemas.openid.net/event/backchannel-logout": {}
}}`,
config: gOidc.Config{
ClientID: "client1",
SkipExpiryCheck: true,
},
signKey: newRSAKey(t),
wantErr: true,
},
{
name: "multiple audiences, one matches",
logoutToken: `{"iss":"https://foo","aud":["client1","client2"],"sub":"subject","events": {
"http://schemas.openid.net/event/backchannel-logout": {}
}}`,
config: gOidc.Config{
ClientID: "client2",
SkipExpiryCheck: true,
},
signKey: newRSAKey(t),
},
}
for _, test := range tests {
t.Run(test.name, test.run)
}
}
type logoutVerificationTest struct {
// Name of the subtest.
name string
// If not provided defaults to "https://foo"
issuer string
// JWT payload (just the claims).
logoutToken string
// Key to sign the ID Token with.
signKey *signingKey
// If not provided defaults to signKey. Only useful when
// testing invalid signatures.
verificationKey *signingKey
config gOidc.Config
wantErr bool
}
type testVerifier struct {
jwk jose.JSONWebKey
}
func (t *testVerifier) VerifySignature(ctx context.Context, jwt string) ([]byte, error) {
jws, err := jose.ParseSigned(jwt)
if err != nil {
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
}
return jws.Verify(&t.jwk)
}
func (v logoutVerificationTest) runGetToken(t *testing.T) (*LogoutToken, error) {
token := v.signKey.sign(t, []byte(v.logoutToken))
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
issuer := "https://foo"
if v.issuer != "" {
issuer = v.issuer
}
var ks gOidc.KeySet
if v.verificationKey == nil {
ks = &testVerifier{v.signKey.jwk()}
} else {
ks = &testVerifier{v.verificationKey.jwk()}
}
verifier := NewLogoutVerifier(issuer, ks, &v.config)
return verifier.Verify(ctx, token)
}
func (l logoutVerificationTest) run(t *testing.T) {
_, err := l.runGetToken(t)
if err != nil && !l.wantErr {
t.Errorf("%v", err)
}
if err == nil && l.wantErr {
t.Errorf("expected error")
}
}
func newRSAKey(t testing.TB) *signingKey {
priv, err := rsa.GenerateKey(rand.Reader, 1028)
if err != nil {
t.Fatal(err)
}
return &signingKey{"", priv, priv.Public(), jose.RS256}
}
func newECDSAKey(t *testing.T) *signingKey {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
return &signingKey{"", priv, priv.Public(), jose.ES256}
}

View File

@@ -8,9 +8,13 @@ import (
"time"
"github.com/coreos/go-oidc/v3/oidc"
ocisLogoutVerifier "github.com/owncloud/ocis/v2/ocis-pkg/oidc"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/token/manager/jwt"
"github.com/go-chi/chi/v5"
chimiddleware "github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/render"
"github.com/justinas/alice"
"github.com/oklog/run"
"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
@@ -39,6 +43,22 @@ import (
"golang.org/x/oauth2"
)
type LogoutHandler struct {
cache microstore.Store
logger log.Logger
config config.Config
}
type LogoutToken struct {
iss string `json:iss` // example "https://server.example.com"
sub int64 `json:sub` //"248289761001"
aud string `json:aud` // "s6BhdRkqt3"
iat int64 `json:iat` // 1471566154
jti string `json:jti` // "bWJq"
sid string `json:sid` // "08a5019c-17e1-4977-8f42-65a12843ea02"
events map[string][]string `json:events` // {"http://schemas.openid.net/event/backchannel-logout": {}}
}
// Server is the entrypoint for the server command.
func Server(cfg *config.Config) *cli.Command {
return &cli.Command{
@@ -49,6 +69,15 @@ func Server(cfg *config.Config) *cli.Command {
return configlog.ReturnFatal(parser.ParseConfig(cfg))
},
Action: func(c *cli.Context) error {
cache := store.Create(
store.Store(cfg.OIDC.UserinfoCache.Store),
store.TTL(cfg.OIDC.UserinfoCache.TTL),
store.Size(cfg.OIDC.UserinfoCache.Size),
microstore.Nodes(cfg.OIDC.UserinfoCache.Nodes...),
microstore.Database(cfg.OIDC.UserinfoCache.Database),
microstore.Table(cfg.OIDC.UserinfoCache.Table),
)
logger := logging.Configure(cfg.Service.Name, cfg.Log)
err := tracing.Configure(cfg)
if err != nil {
@@ -84,13 +113,14 @@ func Server(cfg *config.Config) *cli.Command {
}
{
middlewares := loadMiddlewares(ctx, logger, cfg, cache)
server, err := proxyHTTP.Server(
proxyHTTP.Handler(rp),
proxyHTTP.Handler(handlePredefinedRoutes(cfg, logger, rp, cache, middlewares)),
proxyHTTP.Logger(logger),
proxyHTTP.Context(ctx),
proxyHTTP.Config(cfg),
proxyHTTP.Metrics(metrics.New()),
proxyHTTP.Middlewares(loadMiddlewares(ctx, logger, cfg)),
proxyHTTP.Middlewares(middlewares),
)
if err != nil {
@@ -137,7 +167,50 @@ func Server(cfg *config.Config) *cli.Command {
}
}
func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config) alice.Chain {
func handlePredefinedRoutes(cfg *config.Config, logger log.Logger, handler http.Handler, cache microstore.Store, middlewares alice.Chain) http.Handler {
m := chi.NewMux()
var methods = []string{"PROPFIND", "DELETE", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK"}
for _, k := range methods {
chi.RegisterMethod(k)
}
p := LogoutHandler{
cache: cache,
logger: logger,
config: *cfg,
}
m.Route(cfg.HTTP.Root, func(r chi.Router) {
// Wrapper for backchannel logout
// TODO: remove the GET for the backchannel_logout
r.Get("/backchannel_logout", p.backchannelLogout)
r.Post("/backchannel_logout", p.backchannelLogout)
// TODO: migrate oidc well knowns here in a second wrapper
r.HandleFunc("/*", handler.ServeHTTP)
})
return m
}
func (p *LogoutHandler) backchannelLogout(w http.ResponseWriter, r *http.Request) {
var oidcHTTPClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: p.config.OIDC.Insecure, //nolint:gosec
},
DisableKeepAlives: true,
},
Timeout: time.Second * 10,
}
prov, _ := oidc.NewProvider(
context.WithValue(context.Background(), oauth2.HTTPClient, oidcHTTPClient),
p.config.OIDC.Issuer,
)
logoutVerifier := ocisLogoutVerifier.NewLogoutVerifier(p.config.OIDC)
render.Status(r, http.StatusOK)
}
func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config, cache microstore.Store) alice.Chain {
rolesClient := settingssvc.NewRoleService("com.owncloud.api.settings", grpc.DefaultClient())
revaClient, err := pool.GetGatewayServiceClient(cfg.Reva.Address, cfg.Reva.GetRevaOptions()...)
if err != nil {
@@ -212,15 +285,6 @@ func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config)
})
}
cache := store.Create(
store.Store(cfg.OIDC.UserinfoCache.Store),
store.TTL(cfg.OIDC.UserinfoCache.TTL),
store.Size(cfg.OIDC.UserinfoCache.Size),
microstore.Nodes(cfg.OIDC.UserinfoCache.Nodes...),
microstore.Database(cfg.OIDC.UserinfoCache.Database),
microstore.Table(cfg.OIDC.UserinfoCache.Table),
)
authenticators = append(authenticators, middleware.NewOIDCAuthenticator(
middleware.Logger(logger),
middleware.Cache(cache),

View File

@@ -3,6 +3,7 @@ package middleware
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"strings"
"sync"
@@ -39,6 +40,7 @@ func NewOIDCAuthenticator(opts ...Option) *OIDCAuthenticator {
return &OIDCAuthenticator{
Logger: options.Logger,
userInfoCache: options.Cache,
sessionLookupCache: options.Cache,
DefaultTokenCacheTTL: options.DefaultAccessTokenTTL,
HTTPClient: options.HTTPClient,
OIDCIss: options.OIDCIss,
@@ -56,6 +58,7 @@ type OIDCAuthenticator struct {
HTTPClient *http.Client
OIDCIss string
userInfoCache store.Store
sessionLookupCache store.Store
DefaultTokenCacheTTL time.Duration
ProviderFunc func() (OIDCProvider, error)
AccessTokenVerifyMethod string
@@ -87,7 +90,16 @@ func (m *OIDCAuthenticator) getClaims(token string, req *http.Request) (map[stri
}
m.Logger.Error().Err(err).Msg("could not unmarshal userinfo")
}
aClaims, err := m.verifyAccessToken(token)
// TODO: use mClaims
aClaims, mClaims, err := m.verifyAccessToken(token)
//fmt.Println(mClaims)
vals := make([]string, len(mClaims))
for k, v := range mClaims {
s, _ := base64.StdEncoding.DecodeString(v)
vals[k] = string(s)
}
fmt.Println(vals)
if err != nil {
return nil, errors.Wrap(err, "failed to verify access token")
}
@@ -120,6 +132,17 @@ func (m *OIDCAuthenticator) getClaims(token string, req *http.Request) (map[stri
if err != nil {
m.Logger.Error().Err(err).Msg("failed to write to userinfo cache")
}
if sid, ok := claims["sid"]; ok {
err = m.sessionLookupCache.Write(&store.Record{
Key: fmt.Sprintf("%s", sid),
Value: []byte(encodedHash),
Expiry: time.Until(expiration),
})
}
if err != nil {
m.Logger.Error().Err(err).Msg("failed to write session lookup cache")
}
}
}()
@@ -127,42 +150,52 @@ func (m *OIDCAuthenticator) getClaims(token string, req *http.Request) (map[stri
return claims, nil
}
func (m OIDCAuthenticator) verifyAccessToken(token string) (jwt.RegisteredClaims, error) {
// TODO: update jwt lib to have access to session id, or extract the session id and return it
func (m OIDCAuthenticator) verifyAccessToken(token string) (jwt.RegisteredClaims, []string, error) {
var mapClaims []string
switch m.AccessTokenVerifyMethod {
case config.AccessTokenVerificationJWT:
return m.verifyAccessTokenJWT(token)
case config.AccessTokenVerificationNone:
m.Logger.Debug().Msg("Access Token verification disabled")
return jwt.RegisteredClaims{}, nil
return jwt.RegisteredClaims{}, mapClaims, nil
default:
m.Logger.Error().Str("access_token_verify_method", m.AccessTokenVerifyMethod).Msg("Unknown Access Token verification setting")
return jwt.RegisteredClaims{}, errors.New("Unknown Access Token Verification method")
return jwt.RegisteredClaims{}, mapClaims, errors.New("Unknown Access Token Verification method")
}
}
// verifyAccessTokenJWT tries to parse and verify the access token as a JWT.
func (m OIDCAuthenticator) verifyAccessTokenJWT(token string) (jwt.RegisteredClaims, error) {
func (m OIDCAuthenticator) verifyAccessTokenJWT(token string) (jwt.RegisteredClaims, []string, error) {
var claims jwt.RegisteredClaims
var mapClaims []string
jwks := m.getKeyfunc()
if jwks == nil {
return claims, errors.New("Error initializing jwks keyfunc")
return claims, mapClaims, errors.New("Error initializing jwks keyfunc")
}
_, err := jwt.ParseWithClaims(token, &claims, jwks.Keyfunc)
_, mapClaims, err = new(jwt.Parser).ParseUnverified(token, jwt.MapClaims{})
// TODO: decode mapClaims to sth readable
m.Logger.Debug().Interface("access token", &claims).Msg("parsed access token")
if err != nil {
m.Logger.Info().Err(err).Msg("Failed to parse/verify the access token.")
return claims, err
return claims, mapClaims, err
}
m.Logger.Debug().Interface("access token", &claims).Msg("parsed access token")
if err != nil {
m.Logger.Info().Err(err).Msg("Failed to parse/verify the access token.")
return claims, mapClaims, err
}
if !claims.VerifyIssuer(m.OIDCIss, true) {
vErr := jwt.ValidationError{}
vErr.Inner = jwt.ErrTokenInvalidIssuer
vErr.Errors |= jwt.ValidationErrorIssuer
return claims, vErr
return claims, mapClaims, vErr
}
return claims, nil
return claims, mapClaims, nil
}
// extractExpiration tries to extract the expriration time from the access token