-
Notifications
You must be signed in to change notification settings - Fork 111
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
oauth: add sign-in with apple support (fixes #110) #243
Conversation
One thing I noticed so far trying to test is that Apple doesn't like including the scopes in the auth request unless |
0159567
to
893e82d
Compare
We just needed to redirect POSTs to the existing handler and allow providers to override the method for routing. |
lib/oauth/apple.go
Outdated
teamID, keyID, expiresIn, constructErr := apple.ExtractCredentialData(credentials.Additional) | ||
if constructErr != nil { | ||
return nil, fmt.Errorf("apple credentials: %w", constructErr) | ||
} | ||
|
||
signingKey, publicKey, constructErr := apple.ParsePrivateKey(credentials.SigningKey, keyID) | ||
if constructErr != nil { | ||
return nil, fmt.Errorf("failed to parse signing key: %w", constructErr) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be possible to move apple. ExtractCredentialData
and apple.ParsePrivateKey
to that scope?
The AppleProvider will not function without the necessary credentials and keys. By moving this logic to a higher scope, it will be clearer what needs to be modified, thereby preventing substantial changes to the API. Those values can be stored in the additional
hash map.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Think I prefer leaving it in provider initialization and keeping the app as dumb as possible.
lib/oauth/apple/idtoken_test.go
Outdated
"golang.org/x/oauth2" | ||
) | ||
|
||
func testSigningKey(t *testing.T) (jose.SigningKey, string, *rsa.PrivateKey) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function will be accessible within the apple
package. It might be more effective to encapsulate it within TestGetAppleIDTokenClaims
by creating a variable of the function type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Functions in _test
files aren't callable like that but the change is easy enough to make.
lib/oauth/apple/idtoken_test.go
Outdated
}) | ||
} | ||
|
||
func Test_validateExp(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a typo in the test case name here, please update to use MixedCaps.
lib/oauth/provider.go
Outdated
MethodOverride string | ||
VerificationKey jose.JSONWebKey | ||
} | ||
|
||
// NewProvider returns a properly configured Provider | ||
func NewProvider(config *oauth2.Config, userInfo UserInfoFetcher, signingKey jose.SigningKey) *Provider { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of creating NewProviderWithOverrides
, we could employ the functional options pattern. This approach would centralize the logic in one place, thereby enhancing maintainability.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea!
lib/oauth/provider_test.go
Outdated
"golang.org/x/oauth2" | ||
) | ||
|
||
func TestProvider_Secret(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a typo here in the name of test case "_".
for providerName := range app.OauthProviders { | ||
for providerName, provider := range app.OauthProviders { | ||
var returnRoute *route.Route | ||
if provider.ReturnMethod() == http.MethodPost { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the sake of consistency, I suggest we implement both HTTP methods for all OAuth providers instead of making it conditional.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Think I prefer keeping the surface area as small as possible here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consistency is one of my favorite words, and POST is more secure than the default GET, but it's hard to argue with minimizing the surface area.
Implements an oauth provider for sign-in with apple. Requires some additional flexibility in oauth credentials / providers: - Include a map of additional data in credentials. - Allow providers to override secret behavior - configured secret in apple credentials is a private key used to sign a secret calculated at runtime, in this case a JWT that includes additional data as claims. - Allow providers to accept returns as HTTP POST instead of GET. - Allow providers to add additional oauth options to authorization request.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't reasonably verify the Apple-specific details, so I tried to focus on changes outside of lib/oauth/apple
as well as the JWT validation process.
Would you take a look at the comment on extractUserFromClaims
specifically? The rest is aesthetic, I think.
for providerName := range app.OauthProviders { | ||
for providerName, provider := range app.OauthProviders { | ||
var returnRoute *route.Route | ||
if provider.ReturnMethod() == http.MethodPost { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consistency is one of my favorite words, and POST is more secure than the default GET, but it's hard to argue with minimizing the surface area.
lib/oauth/apple/idtoken.go
Outdated
return claims, nil | ||
} | ||
|
||
func extractUserFromClaims(claims map[string]interface{}, clientID string) (string, string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we use https://pkg.go.dev/github.com/go-jose/go-jose/v3/jwt#Claims.Validate for the standard iss
, aud
, and exp
claims? then our code can focus on our unique values like sub
and email
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will see what I can do here. The problem is if we read into jwt.Claims
we lose visibility into sub
and email
- but there might be something we can do with a custom Claims
type in the apple package that embeds / implements its own json unmarshaler that lets us leverage it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'd normally verify claims like iss
, aud
, and exp
during the decoding step. is it easier to get at sub
and email
if you move the others?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kinda like how this turned out let me know what you think 95e1df3
lib/oauth/apple/pubkey.go
Outdated
type mockKeyStore struct { | ||
keys map[string]*rsa.PublicKey | ||
} | ||
|
||
func (ks *mockKeyStore) get(keyID string) (*rsa.PublicKey, error) { | ||
if key, ok := ks.keys[keyID]; ok { | ||
return key, nil | ||
} | ||
return nil, &keyNotFoundError{keyID: keyID} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should mockKeyStore
be in package apple_test
?
lib/oauth/apple/idtoken_test.go
Outdated
@@ -0,0 +1,291 @@ | |||
package apple |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this be package apple_test
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its testing unexported functions that I'm not sure we need to export - but I did move the mock keyStore into the _test
file.
Moving to apple/test
probably works as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually this got a lot more practical when I implemented that claims suggestion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
claims validation looks good to me!
lib/oauth/apple/claims.go
Outdated
return fmt.Errorf("missing claim 'iat'") | ||
} | ||
|
||
// is default 1m leeway OK here? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'd think so. if there's some network delay when the user returns to authn, this leeway should handle it. if the token is more than 1m too old, then someone's clock is out of sync and they're going to have bigger problems.
return c.Claims.Validate(jwt.Expected{ | ||
Issuer: BaseURL, | ||
Audience: jwt.Audience{clientID}, | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i checked to confirm that it will validate exp
and iat
against time.Now()
by default. 👍
Implements an oauth provider for sign-in with apple. Requires some
additional flexibility in oauth credentials / providers:
apple credentials is a private key used to sign a secret calculated
at runtime, in this case a JWT that includes additional data as claims.
request.