Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
evgenyk committed May 15, 2024
1 parent 055136e commit fe3a969
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 101 deletions.
59 changes: 55 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,64 @@ The Kinde SDK for Go.

## Development

**`<todo>`**
\*\* Kinde Go SDK

- [ ] Include details here on how to develop on this SDK, i.e. contribute to it. E.g., how to install the codebase, get the code running, test locally, build processes, etc. The "Initial set up" section has been added as a start. Please complete it and add to it if necessary.
Requires Go 1.21+

**`</todo>`**
### Usage

### Initial set up
```bash
go get github.com/kinde-oss/kinde-go
go mod tidy
```

## Autorization code flow

```go

```

## Client credentials flow

```go
import (
"github.com/kinde-oss/kinde-go/jwt"
"github.com/kinde-oss/kinde-go/oauth2/client_credentials"
)
```

```go
// Create a new client to use client credentials flow (used for M2M communication)
kindeClient, _ := client_credentials.NewClientCredentialsFlow(
os.Getenv("AUTH_DOMAIN"), //the domain of the authorization server (could be a custom domain)
os.Getenv("CLIENT_ID"), //client_id
os.Getenv("CLIENT_SECRET"), //clienmt_secret
client_credentials.WithKindeManagementAPI(os.Getenv("KINDE_SUB_DOMAIN")), //Kinde subdomain, used to call the management API
client_credentials.WithTokenValidation(
true, //verifies JWKS, establishes key cache
jwt.WillValidateAlgorythm(), //verifying tocken algorythm
),
)
```

#### Manually requesting a token

```go
token, err := kindeClient.GetToken(context.Background())
```

#### Using client to request an API endpoint

```go
//This client will cache the token and re-fetch a new one as it expires
client := kindeClient.GetClient(context.Background())

//example call to Kinde Management API (client needs WithKindeManagementAPI(...))
businessDetails, err := client.Get(fmt.Sprintf("%v/api/v1/business.json", os.Getenv("KINDE_SUB_DOMAIN")))

```

### SDK Development

1. Clone the repository to your machine:

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
require (
github.com/MicahParks/jwkset v0.5.17 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.6.0
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/time v0.5.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17w
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
Expand Down
23 changes: 16 additions & 7 deletions jwt/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ type tokenProcessing struct {
parsed *golangjwt.Token
}

// Token represents a JWT token.
type Token struct {
rawToken *oauth2.Token
processing tokenProcessing
IsTokenValid bool
rawToken *oauth2.Token
processing tokenProcessing
IsValid bool
}

// ParseJwtToken will parse the given token and validate it with the given options.
func ParseJwtToken(rawToken *oauth2.Token, options ...func(*Token)) (*Token, error) {

token := Token{
Expand All @@ -41,7 +43,7 @@ func ParseJwtToken(rawToken *oauth2.Token, options ...func(*Token)) (*Token, err

if err != nil {
errors = append(errors, err)
token.IsTokenValid = false
token.IsValid = false
} else {
claims := parsedToken.Claims.(golangjwt.MapClaims)
isTokenValid := true
Expand All @@ -55,7 +57,7 @@ func ParseJwtToken(rawToken *oauth2.Token, options ...func(*Token)) (*Token, err
isTokenValid = false
}
}
token.IsTokenValid = isTokenValid
token.IsValid = isTokenValid
}
token.processing.parsed = parsedToken

Expand All @@ -79,18 +81,25 @@ func WillResolveCustomJWK(keyFunc func(rawToken string) (interface{}, error)) fu
}
}

// WillValidateWithKeyFunc will validate the token with the given keyFunc.
func ValidateWithKeyFunc(keyFunc func(*golangjwt.Token) (interface{}, error)) func(*Token) {
return func(s *Token) {
s.processing.keyFunc = keyFunc
}
}

func WillValidateAlgorythm() func(*Token) {
// WillValidateAlgorythm will validate the token with the given algorithm, defaults to RS256.
func WillValidateAlgorythm(alg ...string) func(*Token) {
return func(s *Token) {
s.processing.parsingOptions = append(s.processing.parsingOptions, golangjwt.WithValidMethods([]string{"RS256"}))
if len(alg) > 0 {
s.processing.parsingOptions = append(s.processing.parsingOptions, golangjwt.WithValidMethods(alg))
} else {
s.processing.parsingOptions = append(s.processing.parsingOptions, golangjwt.WithValidMethods([]string{"RS256"}))
}
}
}

// WillValidateAudience will validate the audience is present in the token.
func WillValidateAudience(expectedAudience string) func(*Token) {
return func(s *Token) {
f := func(receivedClaims golangjwt.MapClaims) (bool, error) {
Expand Down
93 changes: 6 additions & 87 deletions oauth2/authorization_code/authorization_code.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import (
"fmt"
"net/http"
"net/url"
"slices"
"strings"

keyfunc "github.com/MicahParks/keyfunc/v3"
"github.com/google/uuid"
"github.com/kinde-oss/kinde-go/jwt"
"golang.org/x/oauth2"
)
Expand All @@ -19,10 +18,11 @@ type AuthorizationCodeFlow struct {
authURLOptions url.Values
JWKS_URL string
tokenOptions []func(*jwt.Token)
stateGenerator func() string
}

// Creates a new AuthorizationCodeFlow with the given baseURL, clientID, clientSecret and options to authenticate backend applications.
func NewAuthorizationclientFlow(baseURL string, clientID string, clientSecret string, callbackURL string,
func NewAuthorizationCodeFlow(baseURL string, clientID string, clientSecret string, callbackURL string,
options ...func(*AuthorizationCodeFlow)) (*AuthorizationCodeFlow, error) {
return newAuthorizationCodeflow(baseURL, clientID, clientSecret, callbackURL, options...)
}
Expand Down Expand Up @@ -52,6 +52,9 @@ func newAuthorizationCodeflow(baseURL string, clientID string, clientSecret stri
},
},
authURLOptions: url.Values{},
stateGenerator: func() string {
return fmt.Sprintf("ks_%v", strings.ReplaceAll(uuid.NewString(), "-", ""))
},
}

for _, o := range options {
Expand All @@ -61,90 +64,6 @@ func newAuthorizationCodeflow(baseURL string, clientID string, clientSecret stri
return client, nil
}

// Adds an arbitrary parameter to the list of parameters to request.
func WithAuthParameter(name, value string) func(*AuthorizationCodeFlow) {
return func(s *AuthorizationCodeFlow) {
if val, ok := s.authURLOptions[name]; ok {
if !slices.Contains(val, value) {
s.authURLOptions[name] = append(val, value)
}
} else {
s.authURLOptions[name] = []string{value}
}

}
}

// Adds an audience to the list of audiences to request.
func WithAudience(audience string) func(*AuthorizationCodeFlow) {
return func(s *AuthorizationCodeFlow) {
WithAuthParameter("audience", audience)(s)
}
}

func WithKindeManagementAPI(kindeDomain string) func(*AuthorizationCodeFlow) {
return func(s *AuthorizationCodeFlow) {

asURL, err := url.Parse(kindeDomain)
if err != nil {
return
}

host := asURL.Hostname()
if host == "" {
host = kindeDomain
}

host = strings.TrimSuffix(host, ".kinde.com")
managementApiaudience := fmt.Sprintf("https://%v.kinde.com/api", host)

WithAuthParameter("audience", managementApiaudience)(s)
}
}

// Adds the offline scope to the list of scopes to request.
func WithOffline() func(*AuthorizationCodeFlow) {
return func(s *AuthorizationCodeFlow) {
WithScope("offline")
}
}

// Adds a scope to the list of scopes to request.
func WithScope(scope string) func(*AuthorizationCodeFlow) {
return func(s *AuthorizationCodeFlow) {
s.config.Scopes = append(s.config.Scopes, scope)
}
}

// Returns the URL to redirect the user to authenticate.
func (flow *AuthorizationCodeFlow) GetAuthURL(state string) string {
url, _ := url.Parse(flow.config.AuthCodeURL(state))
query := url.Query()
for k, v := range flow.authURLOptions {
if query.Get(k) == "" {
query[k] = v
}
}
url.RawQuery = query.Encode()
return url.String()
}

// Adds options to validate the token.
func WithTokenValidation(isValidateJWKS bool, tokenOptions ...func(*jwt.Token)) func(*AuthorizationCodeFlow) {
return func(s *AuthorizationCodeFlow) {

if isValidateJWKS {
jwks, err := keyfunc.NewDefault([]string{s.JWKS_URL})
if err != nil {
return
}
s.tokenOptions = append(s.tokenOptions, jwt.ValidateWithKeyFunc(jwks.Keyfunc))
}

s.tokenOptions = append(s.tokenOptions, tokenOptions...)
}
}

// Exchanges the authorization code for a token.
func (flow *AuthorizationCodeFlow) Exchange(ctx context.Context, authorizationCode string) (*jwt.Token, error) {
token, err := flow.config.Exchange(ctx, authorizationCode)
Expand Down
6 changes: 4 additions & 2 deletions oauth2/authorization_code/authorization_code_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ func TestAutorizationCodeFlowOnline(t *testing.T) {
defer testServer.Close()

callbackURL := fmt.Sprintf("%v/callback", testServer.URL)
kindeClient, err := NewAuthorizationclientFlow(
kindeClient, err := NewAuthorizationCodeFlow(
authorizationServer.URL, "b9da18c441b44d81bab3e8232de2e18d", "client_secret", callbackURL,
WithCustomStateGenerator(func() string { return "test_state" }), //custom state generator for testing
WithOffline(), //offline scope
WithAudience("http://my.api.com/api"), //custom API audience
WithKindeManagementAPI("my_kinde_tenant"), //we need kinde tenant domain to generate correct management API audience
Expand All @@ -63,8 +64,9 @@ func TestAutorizationCodeFlowOnline(t *testing.T) {
assert.Contains(t, kindeClient.authURLOptions["audience"], "http://my.api.com/api")
assert.Contains(t, kindeClient.authURLOptions["audience"], "https://my_kinde_tenant.kinde.com/api")

authURL := kindeClient.GetAuthURL("testState")
authURL := kindeClient.GetAuthURL()
assert.NotNil(t, authURL, "AuthURL cannot be null")
assert.Contains(t, authURL, "test_state", "state parameter is missing")

token, err := kindeClient.Exchange(context.Background(), "code")
assert.Nil(t, err, "could not exchange token")
Expand Down
Loading

0 comments on commit fe3a969

Please sign in to comment.