Skip to content

Commit

Permalink
Merge pull request #126 from uselagoon/switch-jwt-library
Browse files Browse the repository at this point in the history
feat: switch JWT library
  • Loading branch information
smlx authored Nov 3, 2022
2 parents b03d315 + 16620b3 commit bcad579
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 67 deletions.
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ module github.com/uselagoon/ssh-portal
go 1.19

require (
github.com/alecthomas/assert/v2 v2.1.0
github.com/alecthomas/kong v0.7.0
github.com/gliderlabs/ssh v0.3.5
github.com/go-sql-driver/mysql v1.6.0
github.com/golang-jwt/jwt/v4 v4.4.2
github.com/google/uuid v1.3.0
github.com/jmoiron/sqlx v1.3.5
github.com/moby/spdystream v0.2.0
Expand All @@ -15,7 +17,6 @@ require (
go.uber.org/zap v1.23.0
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a
gopkg.in/square/go-jose.v2 v2.6.0
k8s.io/api v0.25.3
k8s.io/apimachinery v0.25.3
k8s.io/client-go v0.25.3
Expand All @@ -24,6 +25,7 @@ require (
require (
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/alecthomas/repr v0.1.0 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
Expand All @@ -38,6 +40,7 @@ require (
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
Expand Down
7 changes: 5 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0=
github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA=
github.com/alecthomas/kong v0.7.0 h1:YIjJUiR7AcmHxL87UlbPn0gyIGwl4+nYND0OQ4ojP7k=
github.com/alecthomas/kong v0.7.0/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U=
github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
Expand Down Expand Up @@ -109,6 +111,8 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
Expand Down Expand Up @@ -173,6 +177,7 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
Expand Down Expand Up @@ -598,8 +603,6 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
Expand Down
25 changes: 17 additions & 8 deletions internal/keycloak/client.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Package keycloak implements a client for keycloak which implements
// Lagoon-specific queries.
package keycloak

import (
Expand All @@ -13,11 +15,11 @@ import (
"path"
"time"

"github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
"go.opentelemetry.io/otel"
"go.uber.org/zap"
"golang.org/x/oauth2"
"gopkg.in/square/go-jose.v2/jwt"
)

const pkgName = "github.com/uselagoon/ssh-portal/internal/keycloak"
Expand Down Expand Up @@ -131,14 +133,21 @@ func (c *Client) UserRolesAndGroups(ctx context.Context,
}
c.log.Debug("got user token")
// parse and extract verified attributes
tok, err := jwt.ParseSigned(userToken.AccessToken)
tok, err := jwt.ParseWithClaims(userToken.AccessToken, &SSHAPIClaims{},
func(_ *jwt.Token) (any, error) { return c.jwtPubKey, nil })
if err != nil {
return nil, nil, nil, fmt.Errorf("couldn't parse verified access token: %v", err)
return nil, nil, nil, fmt.Errorf("couldn't parse user account token: %v", err)
}
var attr userAttributes
if err = tok.Claims(c.jwtPubKey, &attr); err != nil {
return nil, nil, nil,
fmt.Errorf("couldn't extract token claims: %v", err)
claims, ok := tok.Claims.(*SSHAPIClaims)
if !ok {
return nil, nil, nil, fmt.Errorf("invalid token claims type: %T", tok.Claims)
}
// Sanity check the AuthorizedParty to confirm the token is for us.
// Keycloak adds this field for token-exchange operations.
// https://openid.net/specs/openid-connect-core-1_0.html#IDToken
if claims.AuthorizedParty != "service-api" {
return nil, nil, nil, fmt.Errorf("invalid azp, expected service-api got %s",
claims.AuthorizedParty)
}
return attr.RealmRoles, attr.UserGroups, attr.GroupProjectIDs, nil
return claims.RealmRoles, claims.UserGroups, claims.GroupProjectIDs, nil
}
58 changes: 29 additions & 29 deletions internal/keycloak/userattributes.go
Original file line number Diff line number Diff line change
@@ -1,44 +1,44 @@
package keycloak

import "encoding/json"
import (
"encoding/json"

type regularAttributes struct {
RealmRoles []string `json:"realm_roles"`
UserGroups []string `json:"group_membership"`
}

// attributes injected into the access token by keycloak
type userAttributes struct {
regularAttributes
GroupProjectIDs map[string][]int
}
"github.com/golang-jwt/jwt/v4"
)

type stringAttributes struct {
GroupPIDs []string `json:"group_lagoon_project_ids"`
}
type groupProjectIDs map[string][]int

func (u *userAttributes) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &u.regularAttributes); err != nil {
return err
}
func (gpids *groupProjectIDs) UnmarshalJSON(data []byte) error {
// unmarshal the double-encoded group-pid attributes
var s stringAttributes
if err := json.Unmarshal(data, &s); err != nil {
var gpas []string
if err := json.Unmarshal(data, &gpas); err != nil {
return err
}
var gpaMaps []map[string][]int
for _, gpa := range s.GroupPIDs {
var gpaMap map[string][]int
if err := json.Unmarshal([]byte(gpa), &gpaMap); err != nil {
// convert the slice of encoded group-pid attributes into a slice of
// group-pid maps
var gpms []map[string][]int
for _, gpa := range gpas {
var gpm map[string][]int
if err := json.Unmarshal([]byte(gpa), &gpm); err != nil {
return err
}
gpaMaps = append(gpaMaps, gpaMap)
gpms = append(gpms, gpm)
}
u.GroupProjectIDs = map[string][]int{}
for _, gpaMap := range gpaMaps {
for k, v := range gpaMap {
u.GroupProjectIDs[k] = v
// flatten the slice of group-pid maps into a single map
*gpids = groupProjectIDs{}
for _, gpm := range gpms {
for k, v := range gpm {
(*gpids)[k] = v
}
}
return nil
}

// SSHAPIClaims contains the relevant claims for use by the SSH API service.
type SSHAPIClaims struct {
RealmRoles []string `json:"realm_roles"`
UserGroups []string `json:"group_membership"`
GroupProjectIDs groupProjectIDs `json:"group_lagoon_project_ids"`
AuthorizedParty string `json:"azp"`
jwt.RegisteredClaims
}
68 changes: 41 additions & 27 deletions internal/keycloak/userattributes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,32 @@ package keycloak

import (
"encoding/json"
"reflect"
"testing"
"time"

"github.com/alecthomas/assert/v2"
"github.com/golang-jwt/jwt/v4"
)

func TestUnmarshalUserAttributes(t *testing.T) {
var testCases = map[string]struct {
input []byte
expect *userAttributes
expect *SSHAPIClaims
}{
"two groups": {
input: []byte(`{
"group_lagoon_project_ids": [
"{\"credentialtest-group1\":[1]}",
"{\"ci-group\":[3,4,5,6,7,8,9,10,11,12,17,14,16,20,21,24,19,23,31]}"]}`),
expect: &userAttributes{
regularAttributes: regularAttributes{
RealmRoles: nil,
UserGroups: nil,
},
expect: &SSHAPIClaims{
RealmRoles: nil,
UserGroups: nil,
GroupProjectIDs: map[string][]int{
"credentialtest-group1": {1},
"ci-group": {3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 17, 14, 16, 20, 21, 24,
19, 23, 31},
},
RegisteredClaims: jwt.RegisteredClaims{},
},
},
"multiple attributes": {
Expand Down Expand Up @@ -73,39 +75,51 @@ func TestUnmarshalUserAttributes(t *testing.T) {
["{\"credentialtest-group1\":[1]}",
"{\"ci-group\":[3,4,5,6,7,8,9,10,11,12,17,14,16,20,21,24,19,23,31]}"]
}`),
expect: &userAttributes{
regularAttributes: regularAttributes{
RealmRoles: []string{
"owner",
"platform-owner",
"offline_access",
"guest",
"reporter",
"developer",
"uma_authorization",
"maintainer"},
UserGroups: []string{
"/ci-group/ci-group-owner",
"/credentialtest-group1/credentialtest-group1-owner"},
},
expect: &SSHAPIClaims{
RealmRoles: []string{
"owner",
"platform-owner",
"offline_access",
"guest",
"reporter",
"developer",
"uma_authorization",
"maintainer"},
UserGroups: []string{
"/ci-group/ci-group-owner",
"/credentialtest-group1/credentialtest-group1-owner"},
GroupProjectIDs: map[string][]int{
"credentialtest-group1": {1},
"ci-group": {3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 17, 14, 16, 20, 21, 24,
19, 23, 31},
},
AuthorizedParty: "service-api",
RegisteredClaims: jwt.RegisteredClaims{
ID: "ba279e79-4f38-43ae-83e7-fe461aad59d1",
Issuer: "http://lagoon-core-keycloak:8080/auth/realms/lagoon",
Subject: "91435afe-ba81-406b-9308-f80b79fae350",
Audience: jwt.ClaimStrings{"account"},
ExpiresAt: &jwt.NumericDate{
Time: time.Date(2021, time.November, 19, 4, 31, 28, 0, time.UTC).Local(),
},
NotBefore: &jwt.NumericDate{
Time: time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC).Local(),
},
IssuedAt: &jwt.NumericDate{
Time: time.Date(2021, time.November, 19, 4, 26, 28, 0, time.UTC).Local(),
},
},
},
},
}
for name, tc := range testCases {
t.Run(name, func(tt *testing.T) {
var ua *userAttributes
err := json.Unmarshal(tc.input, &ua)
var sac *SSHAPIClaims
err := json.Unmarshal(tc.input, &sac)
if err != nil {
tt.Fatal(err)
}
if !reflect.DeepEqual(ua, tc.expect) {
tt.Fatalf("got: %v, expected %v", ua, tc.expect)
}
assert.Equal(tt, sac, tc.expect)
})
}
}

0 comments on commit bcad579

Please sign in to comment.