mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-05-06 15:13:22 -04:00
extract full claims from jwt token to get session id
Signed-off-by: Christian Richter <crichter@owncloud.com>
This commit is contained in:
2
go.mod
2
go.mod
@@ -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
2
go.sum
@@ -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=
|
||||
|
||||
238
ocis-pkg/oidc/logoutverify.go
Normal file
238
ocis-pkg/oidc/logoutverify.go
Normal 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
|
||||
}
|
||||
296
ocis-pkg/oidc/logoutverify_test.go
Normal file
296
ocis-pkg/oidc/logoutverify_test.go
Normal 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}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user