Service Client Tokens #336
Conversation
That's going to get a bit messy, but TBH I'm glad this has bitten us so squarely because it needs to be refactored away sooner or later anyway. The right thing is for us to directly ask the auth-server for the email associated with the account, passing the OAuth bearer token along to authorize the request. |
Based on discussion today, ISTM that we either:
I feel like the constraint has some value so I'd lean slghtly towards (1), but don't feel strongly. @seanmonstar thoughts? |
e61f73e
to
0529a36
|
Woo! tests passed! As in... the missing email field won't blow up the mysql db. It still won't be able to respond with an email until mozilla/fxa-auth-server#915 is merged. |
| @@ -41,7 +41,7 @@ CREATE TABLE IF NOT EXISTS tokens ( | |||
| FOREIGN KEY (clientId) REFERENCES clients(id) ON DELETE CASCADE, | |||
| userId BINARY(16) NOT NULL, | |||
| INDEX tokens_user_id(userId), | |||
| email VARCHAR(256) NOT NULL, | |||
| email VARCHAR(256), | |||
rfk
Sep 29, 2015
Member
I don't know about this intermediate state where some tokens can't fetch the email address...ISTM maybe we should go through with something like mozilla/fxa-auth-server#1053 and then we can just drop this column entirely.
In fact, I wonder if we can deprecate having the email returned implicitly by /verify, as it's a bit of an abstraction violation anyway...
I don't know about this intermediate state where some tokens can't fetch the email address...ISTM maybe we should go through with something like mozilla/fxa-auth-server#1053 and then we can just drop this column entirely.
In fact, I wonder if we can deprecate having the email returned implicitly by /verify, as it's a bit of an abstraction violation anyway...
seanmonstar
Sep 29, 2015
Author
Member
Yea, that'd be ideal. However, I don't know my way around the auth server much, and to completely drop this column, the auth server would need that PR merged, or else ALL requests for email would no longer be serviceable.
I didn't want to block landing this based on that, but if it's possible to get that landed asap, then we could just drop all the email columns in the oauth db.
Yea, that'd be ideal. However, I don't know my way around the auth server much, and to completely drop this column, the auth server would need that PR merged, or else ALL requests for email would no longer be serviceable.
I didn't want to block landing this based on that, but if it's possible to get that landed asap, then we could just drop all the email columns in the oauth db.
| const MAX_TTL_S = config.get('expiration.accessToken') / 1000; | ||
| const GRANT_AUTHORIZATION_CODE = 'authorization_code'; | ||
| const GRANT_REFRESH_TOKEN = 'refresh_token'; | ||
| const GRANT_JWT = 'urn:ietf:params:oauth:grant-type:jwt-bearer'; | ||
|
|
||
| const JWT_AUD = config.get('publicUrl') + '/v1/token'; |
rfk
Sep 29, 2015
Member
Is it usual to have audiences with a path component? I don't recall seeing this pattern used elsewhere. I guess it can't hurt though...
Is it usual to have audiences with a path component? I don't recall seeing this pattern used elsewhere. I guess it can't hurt though...
rfk
Sep 29, 2015
Member
I don't recall seeing this pattern used elsewhere
It's used in both [1] and [2] so I guess I just haven't been paying attention. 👍
[1] https://developers.google.com/identity/protocols/OAuth2ServiceAccount
[2] http://self-issued.info/docs/draft-ietf-oauth-jwt-bearer.html
I don't recall seeing this pattern used elsewhere
It's used in both [1] and [2] so I guess I just haven't been paying attention.
[1] https://developers.google.com/identity/protocols/OAuth2ServiceAccount
[2] http://self-issued.info/docs/draft-ietf-oauth-jwt-bearer.html
| allowed: client.scope, | ||
| requested: payload.scope | ||
| }); | ||
| throw AppError.invalidScopes(); |
rfk
Sep 29, 2015
Member
Pass the scopes to the constructor, for client-visible error message.
Pass the scopes to the constructor, for client-visible error message.
| logger.debug('jwt.invalid.iat', { now: now, iat: payload.iat }); | ||
| throw AppError.invalidAssertion(); | ||
| } | ||
| if ((payload.exp || -Infinity) < now) { |
rfk
Sep 29, 2015
Member
IMHO we should flatly refuse to accept JWTs without an explicit expiry timestamp.
IMHO we should flatly refuse to accept JWTs without an explicit expiry timestamp.
seanmonstar
Sep 29, 2015
Author
Member
This does that, without introducing another branch (which jscs likes to complain about complexity). If there's no payload.exp, then -Infinity is used, and -Infinity < now will always be true, such that this would be considered expired.
This does that, without introducing another branch (which jscs likes to complain about complexity). If there's no payload.exp, then -Infinity is used, and -Infinity < now will always be true, such that this would be considered expired.
rfk
Sep 30, 2015
Member
Ah yes, right you are
Ah yes, right you are
| if ((payload.iat || Infinity) > now) { | ||
| logger.debug('jwt.invalid.iat', { now: now, iat: payload.iat }); | ||
| throw AppError.invalidAssertion(); | ||
| } |
rfk
Sep 29, 2015
Member
Clock skew has caused similar checks to fail surprisingly often. Since this is for server-to-server communication, we can assume reasonably well-synchronised clocks, but I think we should still add some small window for clock skew between the two systems.
Clock skew has caused similar checks to fail surprisingly often. Since this is for server-to-server communication, we can assume reasonably well-synchronised clocks, but I think we should still add some small window for clock skew between the two systems.
rfk
Sep 29, 2015
Member
Also, it's a bit strange that we don't get this expiration-time checking for free from fxa-jwtool. I wonder if there's an API we could add to that library to simplify some of the work here, e.g. a validate() method that we can pass in a timestamp and aud and have it enforce these checks for us. @dannycoates thoughts?
Also, it's a bit strange that we don't get this expiration-time checking for free from fxa-jwtool. I wonder if there's an API we could add to that library to simplify some of the work here, e.g. a validate() method that we can pass in a timestamp and aud and have it enforce these checks for us. @dannycoates thoughts?
seanmonstar
Sep 29, 2015
Author
Member
I've never understood the clock skew problem that well. I know what it is, it just seems that if there is an issue, just push expiration out another couple minutes or something?
I've never understood the clock skew problem that well. I know what it is, it just seems that if there is an issue, just push expiration out another couple minutes or something?
rfk
Oct 1, 2015
Member
It can also be a problem with iat. If the service-client's clock is slightly ahead of the oauth-server's clock, it can generate an assertion that appears to the oauth-server to be from the future, and hence will be rejected. In other situations we've worked around this by checking like if (iat || Infinity > now - 5) just to add a bit of leeway.
It can also be a problem with iat. If the service-client's clock is slightly ahead of the oauth-server's clock, it can generate an assertion that appears to the oauth-server to be from the future, and hence will be rejected. In other situations we've worked around this by checking like if (iat || Infinity > now - 5) just to add a bit of leeway.
rfk
Oct 16, 2015
Member
if (iat || Infinity > now - 5)
Re-reading this, it should be + 5 for "was issued more than five seconds in the future". I still think it's worth adding some small allowance for skew here, we have seen this cause problems in other systems in the past.
if (iat || Infinity > now - 5)
Re-reading this, it should be + 5 for "was issued more than five seconds in the future". I still think it's worth adding some small allowance for skew here, we have seen this cause problems in other systems in the past.
| }).then(function(payload) { | ||
| logger.verbose('jwt.payload', payload); | ||
|
|
||
| var client = SERVICE_CLIENTS[payload.iss]; |
rfk
Sep 29, 2015
Member
IIUC, the iss field here is the client's opaque hex id. I think it would be quite neat if the client didn't need to know this value, and could just put their domain-name in directly like iss: basket.mozilla.org, in a similar manner to what we do for identity assertions. One less piece of coupling between the two systems.
That might complicate the data model though.
IIUC, the iss field here is the client's opaque hex id. I think it would be quite neat if the client didn't need to know this value, and could just put their domain-name in directly like iss: basket.mozilla.org, in a similar manner to what we do for identity assertions. One less piece of coupling between the two systems.
That might complicate the data model though.
seanmonstar
Sep 29, 2015
Author
Member
That's probably doable.
That's probably doable.
seanmonstar
Sep 30, 2015
Author
Member
Ok, here's how I'm doing this: I don't even check for a iss claim anymore.
The verification won't work if the assertion doesn't include a jku in the header that we don't already trust. The current design is that each client has only 1 jku, so I can look up the client based on the jku in the assertion. The only caveat: this requires that every client have a unique jku. No 2 clients can share one. This seems reasonable to me...
Ok, here's how I'm doing this: I don't even check for a iss claim anymore.
The verification won't work if the assertion doesn't include a jku in the header that we don't already trust. The current design is that each client has only 1 jku, so I can look up the client based on the jku in the assertion. The only caveat: this requires that every client have a unique jku. No 2 clients can share one. This seems reasonable to me...
|
@seanmonstar by the way I found [1] while googling for the [1] http://self-issued.info/docs/draft-ietf-oauth-jwt-bearer.html |
| }) | ||
| .then(function(vals) { | ||
| vals.ttl = params.ttl; | ||
| return vals; |
rfk
Sep 29, 2015
Member
This file's getting pretty long and growing a lot of concerns; I think it would benefit from a few high-level comments to assist new readers in navigating the file.
This file's getting pretty long and growing a lot of concerns; I think it would benefit from a few high-level comments to assist new readers in navigating the file.
seanmonstar
Sep 29, 2015
Author
Member
btw I never want to open this file again
btw I never want to open this file again
| if (!uid || !(uid.length === 32 && HEX_STRING.test(uid))) { | ||
| logger.debug('jwt.invalid.sub', uid); | ||
| throw AppError.invalidAssertion(); | ||
| } |
rfk
Sep 29, 2015
Member
In #328 you proposed "id@accounts.firefox.com" for the sub field, but this just uses the raw uid. Any comments on which is superior and why?
(FWIW I'm generally in favour of using namespaced identifeirs, but there's no real ambiguity here so I think the plain id is fine as well. The only thing we'd do if you gave us an unexpected domain in the identifier would be return an error)
In #328 you proposed "id@accounts.firefox.com" for the sub field, but this just uses the raw uid. Any comments on which is superior and why?
(FWIW I'm generally in favour of using namespaced identifeirs, but there's no real ambiguity here so I think the plain id is fine as well. The only thing we'd do if you gave us an unexpected domain in the identifier would be return an error)
seanmonstar
Sep 29, 2015
Author
Member
I don't think 1 is really superior to the other. I think I proposed that because I was looking at the existing browserid module used with fxa's browserid assertions, and then proceeded to forget about when writing the code.
I don't think 1 is really superior to the other. I think I proposed that because I was looking at the existing browserid module used with fxa's browserid assertions, and then proceeded to forget about when writing the code.
|
@seanmonstar this is looking good. I think we should also have #329 ready before merging though, either as part of this PR or separately, since it will make it easier to think through the shape of the feature as a whole. |
| var header = { | ||
| alg: 'RS256', | ||
| typ: 'JWT', | ||
| jku: 'https://basket.accounts.firefox.com/.well-known/jku', |
rfk
Oct 1, 2015
Member
nit: may as well use the real basket domain name of basket.mozilla.org
nit: may as well use the real basket domain name of basket.mozilla.org
| return exports.registerClient({ | ||
| id: client.id, | ||
| name: client.name, | ||
| hashedSecret: encrypt.hash(unique.secret()), |
rfk
Oct 1, 2015
Member
In other places we've used "0000000000000000000000000000000000000000000000000000000000000000" as the hashedSecret for clients that aren't supposed to use a secret. Is that a pattern we should repeat for these service clients for consistency?
In other places we've used "0000000000000000000000000000000000000000000000000000000000000000" as the hashedSecret for clients that aren't supposed to use a secret. Is that a pattern we should repeat for these service clients for consistency?
seanmonstar
Oct 1, 2015
Author
Member
No, that pattern was actually a poor idea, and I clarified in bugs that
those should NOT be the hashes for those clients (cause something can
indeed hash to 0), but that the clients should have random hashes that were
thrown away afterwards.
However, this inserts into the db at startup, which @jrgm may object too.
On Wed, Sep 30, 2015, 10:04 PM Ryan Kelly notifications@github.com wrote:
In lib/db/index.js
#336 (comment)
:
+function serviceClients() {
- var clients = config.get('serviceClients');
- if (clients && clients.length) {
- logger.debug('serviceClients.loading', clients);
- return P.all(clients.map(function(client) {
-
return exports.getClient(client.id).then(function(existing) {
-
if (existing) {
-
logger.verbose('seviceClients.existing', client);
-
return;
-
}
-
return exports.registerClient({
-
id: client.id,
-
name: client.name,
-
hashedSecret: encrypt.hash(unique.secret()),
In other places we've used
"0000000000000000000000000000000000000000000000000000000000000000" as the
hashedSecret for clients that aren't supposed to use a secret. Is that a
pattern we should repeat for these service clients for consistency?
—
Reply to this email directly or view it on GitHub
https://github.com/mozilla/fxa-oauth-server/pull/336/files#r40879361.
No, that pattern was actually a poor idea, and I clarified in bugs that
those should NOT be the hashes for those clients (cause something can
indeed hash to 0), but that the clients should have random hashes that were
thrown away afterwards.
However, this inserts into the db at startup, which @jrgm may object too.
On Wed, Sep 30, 2015, 10:04 PM Ryan Kelly notifications@github.com wrote:
In lib/db/index.js
#336 (comment)
:+function serviceClients() {
- var clients = config.get('serviceClients');
- if (clients && clients.length) {
- logger.debug('serviceClients.loading', clients);
- return P.all(clients.map(function(client) {
return exports.getClient(client.id).then(function(existing) {if (existing) {logger.verbose('seviceClients.existing', client);return;}
return exports.registerClient({id: client.id,name: client.name,hashedSecret: encrypt.hash(unique.secret()),In other places we've used
"0000000000000000000000000000000000000000000000000000000000000000" as the
hashedSecret for clients that aren't supposed to use a secret. Is that a
pattern we should repeat for these service clients for consistency?—
Reply to this email directly or view it on GitHub
https://github.com/mozilla/fxa-oauth-server/pull/336/files#r40879361.
| aud: 'https://oauth.accounts.firefox.com/v1/token', | ||
| iat: now, | ||
| exp: now + (60 * 5), | ||
| sub: userId + '@accounts.firefox.com' |
rfk
Oct 1, 2015
Member
nit: I think this will have to be "@api.accounts.firefox.com" in production, which is pretty ugly, but it's what we have...
nit: I think this will have to be "@api.accounts.firefox.com" in production, which is pretty ugly, but it's what we have...
| // - An options object is passed to `generateTokens()`. | ||
| // - An access_token is generated. | ||
| // - If req.payload.access_type = 'offline', a refresh_token is also | ||
| // generated. |
rfk
Oct 1, 2015
Member
It sounds like this will allow all grant types to generate refresh tokens. I wonder if we should restrict it to only authorization_code clients. Service clients have no legitimate use for a refresh token, since they can generate access tokens on demand, and having them able to generate refresh tokens could make it harder to e.g. thoroughly revoke service client access.
It sounds like this will allow all grant types to generate refresh tokens. I wonder if we should restrict it to only authorization_code clients. Service clients have no legitimate use for a refresh token, since they can generate access tokens on demand, and having them able to generate refresh tokens could make it harder to e.g. thoroughly revoke service client access.
seanmonstar
Oct 1, 2015
Author
Member
Hm, woops. This line is inaccurate. You cannot pass access_type to this route. I'll adjust this line to point out that a refresh token is generated if the authorization_code was created with offline access.
Hm, woops. This line is inaccurate. You cannot pass access_type to this route. I'll adjust this line to point out that a refresh token is generated if the authorization_code was created with offline access.
| } | ||
|
|
||
| return uid; | ||
| } |
rfk
Oct 1, 2015
Member
After sleeping on it, I lean towards allowing raw FxA uids here rather than requiring them to be namespaced. It's the form we've been encouraging clients and reliers to use in other places.
After sleeping on it, I lean towards allowing raw FxA uids here rather than requiring them to be namespaced. It's the form we've been encouraging clients and reliers to use in other places.
seanmonstar
Oct 1, 2015
Author
Member
It has been? Where's an example? I settled on using namespaces, cause they don't hurt, and it's backwards compatible to eventually drop the namespace, but potentially confusing if we need to add one down the road.
It has been? Where's an example? I settled on using namespaces, cause they don't hurt, and it's backwards compatible to eventually drop the namespace, but potentially confusing if we need to add one down the road.
rfk
Oct 9, 2015
Member
It's what we tell reliers to store in their db as the account primary key, for example. Basket and pocket both store it as "" rather than "@api.accounts.firefox.com".
It's what we tell reliers to store in their db as the account primary key, for example. Basket and pocket both store it as "" rather than "@api.accounts.firefox.com".
seanmonstar
Oct 10, 2015
Author
Member
Ok, I'll rip the namespaces back out :)
Ok, I'll rip the namespaces back out :)
rfk
Oct 11, 2015
Member
The other alternative it so accept both forms, but we should probably just keep it simple :-)
The other alternative it so accept both forms, but we should probably just keep it simple :-)
I'm going to kick this over to @jrgm for comment on that one. It seems to me that a reasonable compromise would be to auto-create any clients that do not exist in the database, but not attempt to modify any existing client records in the db. (instead logging a warning for manual review). |
Current code looks up via the client id in the config, and if found, continues the loop (not inserting). It will never edit an exist record. This will happen at every process startup, so logging a warning would happen every time after the first, and weaken the strength of warnings. Also, there shouldn't be anything to manually edit, since the only reason for it to be in the database is for the foreign key restriction. Any changes we might need to make would be in the config (changing the |
I meant log a warning iff the config and the db differ, but yeah, if there's nothing really in the db to edit anyway then it's a bit of a waste of time. |
|
@rfk updated to use plain userids, instead of a namespace. |
|
We chatted with @jrgm briefly today and got tentative support for the "insert but don't modify" approach to handling these clients on startup; assigning back to myself for final review |
| - `code`: A string that was received from the [authorization][] endpoint. | ||
| - If `refresh_token`: | ||
| - `client_id`: The id returned from client registration. |
rfk
Oct 16, 2015
Member
nit: extra indentation here
nit: extra indentation here
|
r+ with a couple of final nits; we'll need a follow-up bug for using mozilla/fxa-auth-server#1070 once it's landed, but assuming the |
|
The failure is because a test in db/mysql is calling process.exit(1), and that seems to be intercepted correctly on node 0.10, but not 0.12? |
This currently works, posting an assertion to
/v1/tokenwith the right properties in the token will give you an access token.One potential blocker that has shown up implementing this:
/v1/email.Closes #328
Closes #329