Skip to content
This repository has been archived by the owner. It is now read-only.

Service Client Tokens #336

Merged
merged 2 commits into from Oct 16, 2015
Merged

Service Client Tokens #336

merged 2 commits into from Oct 16, 2015

Conversation

@seanmonstar
Copy link
Member

@seanmonstar seanmonstar commented Sep 11, 2015

This currently works, posting an assertion to /v1/token with the right properties in the token will give you an access token.

One potential blocker that has shown up implementing this:

  • Since the token is requested without an fxa-assertion, we cannot store the user's email address with the token, and therefore cannot fulfill requests to the profile server for /v1/email.

Closes #328
Closes #329

@rfk
Copy link
Member

@rfk rfk commented Sep 14, 2015

Since the token is requested without an fxa-assertion, we cannot store the user's email address
with the token, and therefore cannot fulfill requests to the profile server for /v1/email

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.

@rfk
Copy link
Member

@rfk rfk commented Sep 15, 2015

Since these clients are only in config, the foreign key restriction on access_tokens.clientId will prevent
inserting these tokens into the MySQL database.

Based on discussion today, ISTM that we either:

  1. Create stub entries for the service clients in the clients table, but keep their special JWK stuff separate so that there's no risk of accidentally allowing other clients to act as service clients.
  2. Just drop the foreign key constraint and be done with it.

I feel like the constraint has some value so I'd lean slghtly towards (1), but don't feel strongly. @seanmonstar thoughts?

@seanmonstar seanmonstar force-pushed the 328-service-client-tokens branch 3 times, most recently from e61f73e to 0529a36 Sep 28, 2015
@seanmonstar
Copy link
Member Author

@seanmonstar seanmonstar commented Sep 28, 2015

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.

@seanmonstar seanmonstar changed the title [wip] Service Client Tokens Service Client Tokens Sep 28, 2015
@seanmonstar seanmonstar assigned rfk and unassigned seanmonstar Sep 28, 2015
@@ -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),

This comment has been minimized.

@rfk

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...

This comment has been minimized.

@seanmonstar

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.

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';

This comment has been minimized.

@rfk

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...

This comment has been minimized.

@rfk

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

allowed: client.scope,
requested: payload.scope
});
throw AppError.invalidScopes();

This comment has been minimized.

@rfk

rfk Sep 29, 2015
Member

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) {

This comment has been minimized.

@rfk

rfk Sep 29, 2015
Member

IMHO we should flatly refuse to accept JWTs without an explicit expiry timestamp.

This comment has been minimized.

@seanmonstar

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 comment has been minimized.

@rfk

rfk Sep 30, 2015
Member

Ah yes, right you are

if ((payload.iat || Infinity) > now) {
logger.debug('jwt.invalid.iat', { now: now, iat: payload.iat });
throw AppError.invalidAssertion();
}

This comment has been minimized.

@rfk

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.

This comment has been minimized.

@rfk

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?

This comment has been minimized.

@seanmonstar

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?

This comment has been minimized.

@rfk

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.

This comment has been minimized.

@rfk

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.

}).then(function(payload) {
logger.verbose('jwt.payload', payload);

var client = SERVICE_CLIENTS[payload.iss];

This comment has been minimized.

@rfk

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.

This comment has been minimized.

@seanmonstar

seanmonstar Sep 29, 2015
Author Member

That's probably doable.

This comment has been minimized.

@seanmonstar

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...

@rfk
Copy link
Member

@rfk rfk commented Sep 29, 2015

@seanmonstar by the way I found [1] while googling for the urn: string used here, is the API here based on that document, or just from the google API?

[1] http://self-issued.info/docs/draft-ietf-oauth-jwt-bearer.html

})
.then(function(vals) {
vals.ttl = params.ttl;
return vals;

This comment has been minimized.

@rfk

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 comment has been minimized.

@seanmonstar

seanmonstar Sep 29, 2015
Author Member

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();
}

This comment has been minimized.

@rfk

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)

This comment has been minimized.

@seanmonstar

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.

@rfk
Copy link
Member

@rfk rfk commented Sep 29, 2015

@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.

@rfk rfk assigned seanmonstar and unassigned rfk Sep 29, 2015
@seanmonstar seanmonstar assigned rfk and unassigned seanmonstar Oct 1, 2015
var header = {
alg: 'RS256',
typ: 'JWT',
jku: 'https://basket.accounts.firefox.com/.well-known/jku',

This comment has been minimized.

@rfk

rfk Oct 1, 2015
Member

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()),

This comment has been minimized.

@rfk

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?

This comment has been minimized.

@seanmonstar

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.

aud: 'https://oauth.accounts.firefox.com/v1/token',
iat: now,
exp: now + (60 * 5),
sub: userId + '@accounts.firefox.com'

This comment has been minimized.

@rfk

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...

// - An options object is passed to `generateTokens()`.
// - An access_token is generated.
// - If req.payload.access_type = 'offline', a refresh_token is also
// generated.

This comment has been minimized.

@rfk

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.

This comment has been minimized.

@seanmonstar

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.

}

return uid;
}

This comment has been minimized.

@rfk

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.

This comment has been minimized.

@seanmonstar

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.

This comment has been minimized.

@rfk

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".

This comment has been minimized.

@seanmonstar

seanmonstar Oct 10, 2015
Author Member

Ok, I'll rip the namespaces back out :)

This comment has been minimized.

@rfk

rfk Oct 11, 2015
Member

The other alternative it so accept both forms, but we should probably just keep it simple :-)

@rfk
Copy link
Member

@rfk rfk commented Oct 1, 2015

However, this inserts into the db at startup, which @jrgm may object too.

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).

@rfk rfk assigned jrgm and unassigned rfk Oct 1, 2015
@seanmonstar
Copy link
Member Author

@seanmonstar seanmonstar commented Oct 1, 2015

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 scope or jku).

@seanmonstar seanmonstar force-pushed the 328-service-client-tokens branch from 1815128 to 763e261 Oct 1, 2015
@rfk
Copy link
Member

@rfk rfk commented Oct 9, 2015

This will happen at every process startup, so logging a warning would happen
every time after the first, and weaken the strength of warnings

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.

@seanmonstar seanmonstar force-pushed the 328-service-client-tokens branch from 763e261 to 84a2661 Oct 13, 2015
@seanmonstar
Copy link
Member Author

@seanmonstar seanmonstar commented Oct 13, 2015

@rfk updated to use plain userids, instead of a namespace.

@rfk
Copy link
Member

@rfk rfk commented Oct 14, 2015

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

@rfk rfk assigned rfk and unassigned jrgm Oct 14, 2015
- `code`: A string that was received from the [authorization][] endpoint.
- If `refresh_token`:
- `client_id`: The id returned from client registration.

This comment has been minimized.

@rfk

rfk Oct 16, 2015
Member

nit: extra indentation here

@rfk
Copy link
Member

@rfk rfk commented Oct 16, 2015

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 MODIFY COLUMN is not a painful migration, we can land this chunk of work in the meantime.

@seanmonstar seanmonstar force-pushed the 328-service-client-tokens branch from 84a2661 to 799f0e2 Oct 16, 2015
@seanmonstar
Copy link
Member Author

@seanmonstar seanmonstar commented Oct 16, 2015

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?

seanmonstar added a commit that referenced this pull request Oct 16, 2015
@seanmonstar seanmonstar merged commit f16fac4 into master Oct 16, 2015
0 of 2 checks passed
0 of 2 checks passed
continuous-integration/travis-ci/push The Travis CI build failed
Details
continuous-integration/travis-ci/pr The Travis CI build is in progress
Details
@seanmonstar seanmonstar deleted the 328-service-client-tokens branch Oct 16, 2015
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

3 participants