Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Include JWKS in B2B constructor too #138

Merged
merged 1 commit into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions stytch/b2b/b2bstytchapi/b2bstytchapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ package b2bstytchapi

import (
"context"
"fmt"
"net/http"
"time"

"github.com/MicahParks/keyfunc/v2"
"github.com/stytchauth/stytch-go/v10/stytch"
"github.com/stytchauth/stytch-go/v10/stytch/b2b"
"github.com/stytchauth/stytch-go/v10/stytch/config"
Expand Down Expand Up @@ -123,6 +126,66 @@ func NewClient(projectID string, secret string, opts ...Option) (*API, error) {
a.Passwords = b2b.NewPasswordsClient(a.client)
a.SSO = b2b.NewSSOClient(a.client)
a.Sessions = b2b.NewSessionsClient(a.client)
// Set up JWKS for local session authentication
httpClient := defaultClient.HTTPClient
if realClient, ok := a.client.(*stytch.DefaultClient); ok {
httpClient = realClient.HTTPClient
}
jwks, err := a.instantiateJWKSClient(httpClient)
if err != nil {
return nil, fmt.Errorf("fetch JWKS from URL: %w", err)
}
a.M2M.JWKS = jwks

return a, nil
}

func (a *API) instantiateJWKSClient(httpClient *http.Client) (*keyfunc.JWKS, error) {
// The context given in the keyfunc Options applies throughout the lifetime of the JWKS
// fetcher. The context we were given here is _only_ for init, so we arrange to cancel the
// JWKS context manually if we couldn't start in time.
jwksCtx, jwksCancel := context.WithCancel(a.initializationContext)

jwkOptions := keyfunc.Options{
Client: httpClient,

// This is the context for ongoing background JWKS fetches. If the keyfunc starts in time,
// it should run until API.Close is called.
Ctx: jwksCtx,

RefreshErrorHandler: func(err error) {
if a.logger != nil {
a.logger.Printf("There was an error with the jwt.Keyfunc\nError: %s", err.Error())
}
},
RefreshInterval: time.Hour,
RefreshRateLimit: 5 * time.Minute,
RefreshTimeout: 10 * time.Second,
RefreshUnknownKID: true,
}

jwksURL := fmt.Sprintf("%s/v1/sessions/jwks/%s", a.baseURI, a.projectID)

type Res struct {
jwks *keyfunc.JWKS
err error
}
res := make(chan Res)
go func() {
jwks, err := keyfunc.Get(jwksURL, jwkOptions)
res <- Res{jwks, err}
}()

select {
case <-a.initializationContext.Done():
// Couldn't start in time, clean up the JWKS.
jwksCancel()
return nil, a.initializationContext.Err()
case res := <-res:
// JWKS setup finished first, _do not_ cancel its context. Let it continue fetching in the
// background.
_ = jwksCancel // lostcancel

return res.jwks, res.err
}
}
2 changes: 0 additions & 2 deletions stytch/b2b/mfa/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ package mfa
// or your changes may be overwritten later!
// !!!

// MemberOptions:
type MemberOptions struct {
// MFAPhoneNumber: The Member's MFA phone number.
MFAPhoneNumber string `json:"mfa_phone_number,omitempty"`
}

Expand Down
36 changes: 7 additions & 29 deletions stytch/b2b/organizations/members/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,16 @@ type CreateParams struct {
// for emergency purposes to gain access outside of normal authentication procedures. Refer to the
// [Organization object](organization-object) and its `auth_methods` and `allowed_auth_methods` fields for
// more details.
IsBreakglass bool `json:"is_breakglass,omitempty"`
// MFAPhoneNumber: (Coming Soon) The Member's phone number. A Member may only have one phone number.
IsBreakglass bool `json:"is_breakglass,omitempty"`
MFAPhoneNumber string `json:"mfa_phone_number,omitempty"`
// MFAEnrolled: (Coming Soon) Sets whether the Member is enrolled in MFA. If true, the Member must complete
// an MFA step whenever they wish to log in to their Organization. If false, the Member only needs to
// complete an MFA step if the Organization's MFA policy is set to `REQUIRED_FOR_ALL`.
MFAEnrolled bool `json:"mfa_enrolled,omitempty"`
}

// DeleteMFAPhoneNumberParams: Request type for `Members.DeleteMFAPhoneNumber`.
type DeleteMFAPhoneNumberParams struct {
// OrganizationID: Globally unique UUID that identifies a specific Organization. The `organization_id` is
// critical to perform operations on an Organization, so be sure to preserve this value.
OrganizationID string `json:"organization_id,omitempty"`
// MemberID: Globally unique UUID that identifies a specific Member. The `member_id` is critical to perform
// operations on a Member, so be sure to preserve this value.
MemberID string `json:"member_id,omitempty"`
MemberID string `json:"member_id,omitempty"`
}

// DeleteParams: Request type for `Members.Delete`.
Expand Down Expand Up @@ -134,11 +127,7 @@ type UpdateParams struct {
// for emergency purposes to gain access outside of normal authentication procedures. Refer to the
// [Organization object](organization-object) and its `auth_methods` and `allowed_auth_methods` fields for
// more details.
IsBreakglass bool `json:"is_breakglass,omitempty"`
// MFAPhoneNumber: (Coming Soon) Sets the Member's phone number. Throws an error if the Member already has
// a phone number. To change the Member's phone number, use the
// [Delete member phone number endpoint](https://stytch.com/docs/b2b/api/delete-member-mfa-phone-number) to
// delete the Member's existing phone number first.
IsBreakglass bool `json:"is_breakglass,omitempty"`
MFAPhoneNumber string `json:"mfa_phone_number,omitempty"`
// MFAEnrolled: (Coming Soon) Sets whether the Member is enrolled in MFA. If true, the Member must complete
// an MFA step whenever they wish to log in to their Organization. If false, the Member only needs to
Expand All @@ -163,23 +152,12 @@ type CreateResponse struct {
// are server errors.
StatusCode int32 `json:"status_code,omitempty"`
}

// DeleteMFAPhoneNumberResponse: Response type for `Members.DeleteMFAPhoneNumber`.
type DeleteMFAPhoneNumberResponse struct {
// RequestID: Globally unique UUID that is returned with every API call. This value is important to log for
// debugging purposes; we may ask for this value to help identify a specific API call when helping you
// debug an issue.
RequestID string `json:"request_id,omitempty"`
// MemberID: Globally unique UUID that identifies a specific Member.
MemberID string `json:"member_id,omitempty"`
// Member: The [Member object](https://stytch.com/docs/b2b/api/member-object).
Member organizations.Member `json:"member,omitempty"`
// Organization: The [Organization object](https://stytch.com/docs/b2b/api/organization-object).
RequestID string `json:"request_id,omitempty"`
MemberID string `json:"member_id,omitempty"`
Member organizations.Member `json:"member,omitempty"`
Organization organizations.Organization `json:"organization,omitempty"`
// StatusCode: The HTTP status code of the response. Stytch follows standard HTTP response status code
// patterns, e.g. 2XX values equate to success, 3XX values are redirects, 4XX are client errors, and 5XX
// are server errors.
StatusCode int32 `json:"status_code,omitempty"`
StatusCode int32 `json:"status_code,omitempty"`
}

// DeletePasswordResponse: Response type for `Members.DeletePassword`.
Expand Down
5 changes: 3 additions & 2 deletions stytch/b2b/organizations/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,10 @@ type Member struct {
// OrganizationID: Globally unique UUID that identifies a specific Organization. The `organization_id` is
// critical to perform operations on an Organization, so be sure to preserve this value.
OrganizationID string `json:"organization_id,omitempty"`
// MemberID: Globally unique UUID that identifies a specific Member.
// MemberID: Globally unique UUID that identifies a specific Member. The `member_id` is critical to perform
// operations on a Member, so be sure to preserve this value.
MemberID string `json:"member_id,omitempty"`
// EmailAddress: The email address.
// EmailAddress: The email address of the Member.
EmailAddress string `json:"email_address,omitempty"`
// Status: The status of the Member. The possible values are: `pending`, `invited`, `active`, or `deleted`.
Status string `json:"status,omitempty"`
Expand Down
1 change: 0 additions & 1 deletion stytch/b2b/organizations_members.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ func (c *OrganizationsMembersClient) Delete(
return &retVal, err
}

// DeleteMFAPhoneNumber: Delete a Member's MFA phone number.
func (c *OrganizationsMembersClient) DeleteMFAPhoneNumber(
ctx context.Context,
body *members.DeleteMFAPhoneNumberParams,
Expand Down
4 changes: 1 addition & 3 deletions stytch/b2b/otp/sms/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,7 @@ type SendParams struct {
OrganizationID string `json:"organization_id,omitempty"`
// MemberID: Globally unique UUID that identifies a specific Member. The `member_id` is critical to perform
// operations on a Member, so be sure to preserve this value.
MemberID string `json:"member_id,omitempty"`
// MFAPhoneNumber: The phone number to send the OTP to. If the Member already has a phone number, this
// argument is not needed.
MemberID string `json:"member_id,omitempty"`
MFAPhoneNumber string `json:"mfa_phone_number,omitempty"`
// Locale: Used to determine which language to use when sending the user this delivery method. Parameter is
// a [IETF BCP 47 language tag](https://www.w3.org/International/articles/language-tags/), e.g. `"en"`.
Expand Down
13 changes: 3 additions & 10 deletions stytch/b2b/otp_sms.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,11 @@ func NewOTPsSmsClient(c stytch.Client) *OTPsSmsClient {

// Send a one-time passcode (OTP) to a Member's phone number. If the Member already has a phone number,
// this will send an OTP to the number associated with their `member_id`. If not, then this will send an
// OTP to the `mfa_phone_number` provided and link the `mfa_phone_number` with the Member.
// An error will be thrown if the Member already has a phone number and the provided `mfa_phone_number`
// does not match the existing one.
// OTP to the `phone_number` provided and link the `phone_number` with the Member.
// An error will be thrown if the Member already has a phone number and the provided `phone_number` does
// not match the existing one.
//
// Note that sending another OTP code before the first has expired will invalidate the first code.
//
// If a Member has a phone number and is enrolled in MFA, then after a successful primary authentication
// event (e.g. [email magic link](https://stytch.com/docs/b2b/api/authenticate-magic-link) or
// [SSO](https://stytch.com/docs/b2b/api/sso-authenticate) login is complete), an SMS OTP will
// automatically be sent to their phone number. In that case, this endpoint should only be used for
// subsequent authentication events, such as prompting a Member for an OTP again after a period of
// inactivity.
func (c *OTPsSmsClient) Send(
ctx context.Context,
body *sms.SendParams,
Expand Down
2 changes: 1 addition & 1 deletion stytch/config/version.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package config

const APIVersion = "10.1.0"
const APIVersion = "10.1.1"
3 changes: 2 additions & 1 deletion stytch/consumer/stytchapi/stytchapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,10 @@ func NewClient(projectID string, secret string, opts ...Option) (*API, error) {
if err != nil {
return nil, fmt.Errorf("fetch JWKS from URL: %w", err)
}
a.Sessions.JWKS = jwks
a.M2M.JWKS = jwks

a.Sessions.JWKS = jwks

return a, nil
}

Expand Down
Loading