fix: jwt profile request in op

This commit is contained in:
adlerhurst 2020-09-16 14:12:41 +02:00
parent d97df8a9b2
commit fd3daa2335
8 changed files with 224 additions and 16 deletions

8
go.mod
View file

@ -5,7 +5,7 @@ go 1.15
require (
github.com/caos/logging v0.0.0-20191210002624-b3260f690a6a
github.com/golang/mock v1.4.4
github.com/google/go-cmp v0.4.1 // indirect
github.com/google/go-cmp v0.5.2 // indirect
github.com/google/go-github/v31 v31.0.0
github.com/google/uuid v1.1.2
github.com/gorilla/handlers v1.5.0
@ -15,11 +15,11 @@ require (
github.com/kr/pretty v0.1.0 // indirect
github.com/sirupsen/logrus v1.6.0
github.com/stretchr/testify v1.6.1
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 // indirect
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933
golang.org/x/net v0.0.0-20200822124328-c89045814202
golang.org/x/oauth2 v0.0.0-20191122200657-5d9234df094c
golang.org/x/text v0.3.3
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.5 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/square/go-jose.v2 v2.5.1
)

20
go.sum
View file

@ -12,8 +12,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo=
github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
@ -52,16 +52,16 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 h1:anGSYQpPhQwXlwsu5wmfq0nWkCNaMEMUwAv13Y92hd8=
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5PW3vNmnN64U2ZW759Lk=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20191122200657-5d9234df094c h1:HjRaKPaiWks0f5tA6ELVF7ZfqSppfPwOEEAvsrKUTO4=
golang.org/x/oauth2 v0.0.0-20191122200657-5d9234df094c/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -72,6 +72,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab h1:FvshnhkKW+LO3HWHodML8kuVX8rnJTxKm9dFPuI68UM=
golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@ -81,14 +83,16 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=

View file

@ -22,6 +22,10 @@ type TokenExchangeRequest struct {
requestedTokenType string `schema:"requested_token_type"`
}
type JWTProfileRequest struct {
assertion string `schema:"assertion"`
}
func NewTokenExchangeRequest(subjectToken, subjectTokenType string, opts ...TokenExchangeOption) *TokenExchangeRequest {
t := &TokenExchangeRequest{
grantType: TokenExchangeGrantType,

View file

@ -59,6 +59,30 @@ type IDTokenClaims struct {
Signature jose.SignatureAlgorithm //TODO: ???
}
type JWTProfileAssertion struct {
PrivateKeyID string
PrivateKey []byte
Scopes []string
Issuer string
Subject string
Audience []string
Expiration time.Time
IssuedAt time.Time
}
func NewJWTProfileAssertion(userID, keyID string, audience []string, key []byte) *JWTProfileAssertion {
return &JWTProfileAssertion{
PrivateKey: key,
PrivateKeyID: keyID,
Issuer: userID,
Scopes: []string{ScopeOpenID},
Subject: userID,
IssuedAt: time.Now().UTC(),
Expiration: time.Now().Add(1 * time.Hour).UTC(),
Audience: audience,
}
}
type jsonToken struct {
Issuer string `json:"iss,omitempty"`
Subject string `json:"sub,omitempty"`
@ -213,6 +237,34 @@ func (t *IDTokenClaims) SetSignature(alg jose.SignatureAlgorithm) {
t.Signature = alg
}
func (t *JWTProfileAssertion) MarshalJSON() ([]byte, error) {
j := jsonToken{
Issuer: t.Issuer,
Subject: t.Subject,
Audiences: t.Audience,
Expiration: timeToJSON(t.Expiration),
IssuedAt: timeToJSON(t.IssuedAt),
Scopes: strings.Join(t.Scopes, " "),
}
return json.Marshal(j)
}
func (t *JWTProfileAssertion) UnmarshalJSON(b []byte) error {
var j jsonToken
if err := json.Unmarshal(b, &j); err != nil {
return err
}
t.Issuer = j.Issuer
t.Subject = j.Subject
t.Audience = audienceFromJSON(j.Audiences)
t.Expiration = time.Unix(j.Expiration, 0).UTC()
t.IssuedAt = time.Unix(j.IssuedAt, 0).UTC()
t.Scopes = strings.Split(j.Scopes, " ")
return nil
}
func (j *jsonToken) UnmarshalUserinfoProfile() UserinfoProfile {
locale, _ := language.Parse(j.Locale)
return UserinfoProfile{

View file

@ -105,7 +105,7 @@ func (i *Userinfo) UnmmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, i); err != nil {
return err
}
return json.Unmarshal(data, i.claims)
return json.Unmarshal(data, &i.claims)
}
type jsonUserinfo struct {

View file

@ -17,6 +17,7 @@ const (
idTokenKey = "id_token"
stateParam = "state"
pkceCode = "pkce"
jwtProfileKey = "urn:ietf:params:oauth:grant-type:jwt-bearer"
)
//RelayingParty declares the minimal interface for oidc clients
@ -346,6 +347,24 @@ func CallTokenEndpoint(request interface{}, rp RelayingParty) (newToken *oauth2.
return token, nil
}
func CallJWTProfileEndpoint(assertion string, rp RelayingParty) (*oauth2.Token, error) {
form := make(map[string][]string)
form["assertion"] = []string{assertion}
form["grant_type"] = []string{jwtProfileKey}
req, err := http.NewRequest("POST", rp.OAuthConfig().Endpoint.TokenURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
token := new(oauth2.Token)
if err := utils.HttpRequest(rp.HttpClient(), req, token); err != nil {
return nil, err
}
return token, nil
}
func trySetStateCookie(w http.ResponseWriter, state string, rp RelayingParty) error {
if rp.CookieHandler() != nil {
if err := rp.CookieHandler().SetCookie(w, stateParam, state); err != nil {

View file

@ -2,9 +2,15 @@ package rp
import (
"context"
"crypto/rsa"
"crypto/x509"
"encoding/json"
"encoding/pem"
"golang.org/x/oauth2"
"gopkg.in/square/go-jose.v2"
"github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/oidc/grants/tokenexchange"
)
@ -37,3 +43,54 @@ func TokenExchange(ctx context.Context, request *tokenexchange.TokenExchangeRequ
func DelegationTokenExchange(ctx context.Context, subjectToken string, rp RelayingParty, reqOpts ...tokenexchange.TokenExchangeOption) (newToken *oauth2.Token, err error) {
return TokenExchange(ctx, DelegationTokenRequest(subjectToken, reqOpts...), rp)
}
func JWTProfileExchange(ctx context.Context, assertion *oidc.JWTProfileAssertion, rp RelayingParty) (*oauth2.Token, error) {
token, err := generateJWTProfileToken(assertion)
if err != nil {
return nil, err
}
return CallJWTProfileEndpoint(token, rp)
}
func generateJWTProfileToken(assertion *oidc.JWTProfileAssertion) (string, error) {
privateKey, err := bytesToPrivateKey(assertion.PrivateKey)
if err != nil {
return "", err
}
key := jose.SigningKey{
Algorithm: jose.RS256,
Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID},
}
signer, err := jose.NewSigner(key, &jose.SignerOptions{})
if err != nil {
return "", err
}
jsonadsk, err := json.Marshal(assertion)
if err != nil {
return "", err
}
signiert, err := signer.Sign(jsonadsk)
if err != nil {
return "", err
}
return signiert.CompactSerialize()
}
func bytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) {
block, _ := pem.Decode(priv)
enc := x509.IsEncryptedPEMBlock(block)
b := block.Bytes
var err error
if enc {
b, err = x509.DecryptPEMBlock(block, nil)
if err != nil {
return nil, err
}
}
key, err := x509.ParsePKCS1PrivateKey(b)
if err != nil {
return nil, err
}
return key, nil
}

View file

@ -0,0 +1,72 @@
package rp
import (
"testing"
"github.com/caos/oidc/pkg/oidc"
)
func TestGenerateJWTProfileToken(t *testing.T) {
type args struct {
assertion *oidc.JWTProfileAssertion
}
type res struct {
wantErr bool
// token string
}
tests := []struct {
name string
args args
res res
}{
{
name: "valid",
args: args{
assertion: oidc.NewJWTProfileAssertion("service-account-id", "key-id", []string{"http://localhost:50002/oauth/v2/"}, []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAsqxKigKty6jBIuDtO9AIGdTNo0VMe/QoYlHPxDQtgKhdXjD7
GNVLlr4qy+28Slo/Nph5/UFIc6BhXUGCK0JSRDoXukB36UnZblT/xyjz9NLzAEsk
HKCBhkJL7uuSRDvj9L367/02QhSzYh00vi6sG+d4cUuD3n4oZwf0IifZy4OJBX2R
SX2sV8Lh3eR39UrnA2qiwRPQmToVPu1x18BphvE9kZtVzTvy/d0VXtEcNJDvYNuH
kYM3DLzzR0+DCDS8bo7IaX8i1TQoO8y5rXlZ5C7+FdAa15lt5N+A0Lfz7hu45s+r
OCcp2s6IvsRZKV4HWhEhnQuoRdXQmpnQGlFnFQIDAQABAoIBABY41YB6utDkqTjE
Tt0sj4Ve8UCIQu37vPYVhMi7UJl61zn6z5AUHzWda0c3xz5cIRaSOkHkV7WB0fo+
RolI02CG9SKGGCPcun09dx53GnhtsCluLwycbd+b6UPK6sMvy7dJ1ab5kEEBwBnI
1iF9Poyt6k30/W6ztCS0WYnR+QWVnhtMPBRtzeSAb8OdKRQsNRKWgN+BK7VzCFls
jr4tvwdq/5WWLmYYqeteVK8nt9muJGUdmqS8UBf0D1Qo2/Ob+UKIE3ff/VlaEW+g
YhVkKspUYkuOWERk4awEkExZ0zoTkEKv4BgL53a41KO3UV8GIkSW8W8SfIL1aIEt
aPvm6XUCgYEA57HtG2GWJIozBbBlxT1UC2I0hFW37+JETx7ingcsjNxobQZftWZc
36PwZ4kmLxLJIinuFdEK7ghfm4123PUaKxH4gNXMzO7rzF2pskD902uaDijpkd1c
asXSIzy7MnG2ipEmxbqD3wPa7Lm9RCwl1ldCfOkZpkilsgBEAkWGtosCgYEAxWp7
F4GlcI1S6/lUdxDeem5QTEYLuE45prQhT0jiFzxDT3kuX4mK/4A7wryPif+UCksf
U5cbo65l+IYc3JUx2DtZARHALdg7BHiaYqsx3qWVM0OokwEstbSrLT6+jSolegSf
+9LghTS5ZowkighyiW5OsJMIehuHowdSH9NZrN8CgYA6XHsZNo+XTKhlenVoJXaS
F36bBux6JEiIlYMHw07ZfHthWwWor8wdGTJpIgbYPKclT+KE5E8Yfkt25z9VkPey
eaha63/W7ye+JqmkGPLW2nfHsU6ES3oH+yRfc+DDaBlO9hkKHV0yQ8pVbsPZ9DTj
tL8ur5iiZhI2sBJxcAnq2QKBgQC4DnTBD8DdVQXQuF9Fu1aRszPuSQg4R8Z8ZEkC
EKOqoibne8X+kNAlMruE7iSttrmhdzS3zJSaYMj1kqRqDDeysHJlCtWwaH9txbu6
7n3KZXrbluMeW+QBbXaC8pLaLkdOoe0+7fciemu47kRK5WFUPKHlAtDOd8hX+UVa
IsTi5QKBgCCfYiuTdesjMKAc3UQ0zjVYAxS1HqJh7V5McqscFx3D3uxg1JJcojwc
QOsG4gLgcCbr3kBkQLDHC+ZTjKgJ7OTVUf6To5yz2WlrDL+0YN+TmA8fpQR/rL2y
VgGFLpjDn3JtxGFwhV2CCwrC/ZCSut2L8IGi5+KtwMUW8clN9skR
-----END RSA PRIVATE KEY-----
`)),
},
res: res{
wantErr: false,
// token: "eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS1pZCJ9.eyJpc3MiOiJzZXJ2aWNlLWFjY291bnQtaWQiLCJzdWIiOiJzZXJ2aWNlLWFjY291bnQtaWQiLCJhdWQiOlsiaHR0cDovL2xvY2FsaG9zdDo1MDAwMi9vYXV0aC92Mi8iXSwiZXhwIjoxNjAwMjYxNDQyLCJpYXQiOjE2MDAyNTc4NDIsInNjb3BlIjoib3BlbmlkIn0.c9w4al8NfWboMbp9U4j34SnMFei_RH7ajowE-B6GT0mTZRoSRR5lldti6aFObbyraYbpEbTmrH3LalSVTeQwkD0tRKMll3pNyHZ0OtsZQEVLKLtFZG5F3lmB5sbuRLkIRmh7lRad1o9NR2PVqCMJjbBqPRaUADhUaWnY5oTHt5xdt_T--VJfo871TG_Hcp8J-uAvyDqzccX6jrx4jG0t_q1ps1EUwkdzILb_ezv3PTb3YF9sj1lNuDs4dOJGKjBJNdHvhO_ofNMfb6wewmXF5hzZgO72PC7h1Pr__xRcuemO4VIpC1lb24lyYbM1Yg3m5z_e0ByU4dI0ePh-FPBsWQ",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := generateJWTProfileToken(tt.args.assertion)
if (err != nil) != tt.res.wantErr {
t.Errorf("generateJWTProfileToken() error = %v, wantErr %v", err, tt.res.wantErr)
return
}
// if !reflect.DeepEqual(got, tt.res.token) {
// t.Errorf("generateJWTProfileToken() = %q, want %q", got, tt.res.token)
// }
})
}
}