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

Add full OAuth2 server for gitpod local control to use #4164

Closed
wants to merge 102 commits into from

Conversation

rl-gitpod
Copy link
Contributor

No description provided.

@rl-gitpod
Copy link
Contributor Author

rl-gitpod commented May 7, 2021

/werft run

👍 started the job as gitpod-build-rl-gplctl-oauth2-server.9

@rl-gitpod
Copy link
Contributor Author

rl-gitpod commented May 8, 2021

/werft run

👍 started the job as gitpod-build-rl-gplctl-oauth2-server.10

@rl-gitpod
Copy link
Contributor Author

rl-gitpod commented May 9, 2021

/werft run

👍 started the job as gitpod-build-rl-gplctl-oauth2-server.21

@rl-gitpod
Copy link
Contributor Author

rl-gitpod commented May 10, 2021

/werft run

👍 started the job as gitpod-build-rl-gplctl-oauth2-server.32

@rl-gitpod
Copy link
Contributor Author

rl-gitpod commented May 10, 2021

/werft run

👍 started the job as gitpod-build-rl-gplctl-oauth2-server.33

@rl-gitpod
Copy link
Contributor Author

rl-gitpod commented May 11, 2021

/werft run

👍 started the job as gitpod-build-rl-gplctl-oauth2-server.37

@rl-gitpod rl-gitpod force-pushed the rl/gplctl-oauth2-server branch 2 times, most recently from a39a280 to 9ffc0b6 Compare May 13, 2021 23:08
Copy link

@Greeneisgood Greeneisgood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping

@rl-gitpod rl-gitpod force-pushed the rl/gplctl-oauth2-server branch from 4621c07 to 3119a38 Compare May 15, 2021 09:03
@rl-gitpod rl-gitpod force-pushed the rl/gplctl-oauth2-server branch from 640390d to c67755e Compare May 16, 2021 01:25
user,
client,
redirectUri: "",
codeChallenge: undefined,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought the plan was to use PKCE, no? This looks like it's disabled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a somewhat odd approach, but the oauth lib populates it, along with the redirectUri, scopes, etc as part of its flow

That said, I've added a comment, removed the confusing params and added code in the oauth server constructor to make the PKCE-side more explicit:

const authorizationServer = new GitpodAuthorizationServer(
    authCodeRepository,
    clientRepository,
    tokenRepository,
    scopeRepository,
    userRepository,
    jwtService,
    {
      // Be explicit, communicate intent. Default is true but let's not assume that
      requiresPKCE: true, 
    }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

}
});
}
public issueAuthCode(client: OAuthClient, user: OAuthUser | undefined, scopes: OAuthScope[]): OAuthAuthCode {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We used to separate services and persistence. If applied, methods like issueAuthCode would go to a service module which in turn uses the repository implementation.

authCodes = authCodes.filter(te => (new Date(te.expiresAt)).getTime() > Date.now());
log.info(`getByIdentifier post: ${JSON.stringify(authCodes)}`);
const authCode = authCodes.length > 0 ? authCodes[0] : undefined;
return new Promise<OAuthAuthCode>((resolve, reject) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in async functions you don't need to return a promise, instead:

if (!authCode) {
  throw new Error(...);
}
return authCode;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, that was cruft - removed.

const authCodeRepo = await this.getOauthAuthCodeRepo();
authCodeRepo.save(authCode);
}
public async isRevoked(authCodeCode: string): Promise<boolean> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like unused. please remove if true.
also the implementation looks more like isExpired

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Def called by lib and is an important part of the required interface.

Agreed that it looks like isExpired because that is exactly what it is doing :-)
Given we are very unlikely to want to support long-lived auth tokens I didn't want to go to the trouble of extending the interface to add something more suitable. The intention is to add a cleanup phase to remove expired auth codes at a later date.

if (authCode) {
log.info(`revoke auth ${authCodeCode} ${JSON.stringify(authCode)}`);
// Set date to earliest timestamp that MySQL allows
authCode.expiresAt = new Date(1000);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why expiring the authCode?

  1. auth codes are short-living anyways
  2. that's not used

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why expiring the authCode?
See answer above.

  1. auth codes are short-living anyways
    Best practice - yes, but not always and we are using a general purpose lib so it allows for that more unsafe option.
  1. that's not used
    See answer above.

dbToken = {
tokenHash,
name: `local-app`,
type: GitpodTokenType.MACHINE_AUTH_TOKEN,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@csweichel, what's the meaning of MACHINE_AUTH_TOKEN? this sounds like a new type.

log.info(`persist access token ${JSON.stringify(accessToken)}`);
var scopes: string[] = [];
for (const scope of accessToken.scopes) {
scopes = scopes.concat(scope.name);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: shorter: const scopes = [...accessToken.scopes];

Copy link
Contributor Author

@rl-gitpod rl-gitpod May 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks... that fails with a type mismatch further down, but it is now the more concise:

const scopes = accessToken.scopes.map((s) => s.name);

}

// Have they authorized the local-app?
const wasApproved = req.query['approved'] || '';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how is the approved param related to the authorization request? is that a shortcut for the interactive part, i.e. the web flow?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not so much a shortcut, but yes it is part of the web flow, in particular the rejection side so it returns immediately.

It is probably better to keep the explicit rejection in the db as opposed to deleting the entry. That way we can differentiate between not having asked for approval and an explicit rejection. The downside is that if they've changed their mind after initially rejecting it we have no easy way for them to do that in the UI... yet. It can be done via an explicitly formed URL but is not user-friendly.

res.redirect(redirectTo)
return;
} else {
log.error(`/oauth/authorize unknown client id: "${clientID}"`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: looks like unreachable

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, using the hard-coded client id.
It should, and now does, use the requested client id which can fail.

});

router.post("/oauth/token", async (req: express.Request, res: express.Response) => {
log.info(`TOKEN: ${JSON.stringify(req.body)}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it's the code at this point

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand the comment, can you elaborate pls?

@rl-gitpod
Copy link
Contributor Author

@AlexTugarev - I just realised you had reviewed the old, many-commits-for-testing PR :-(
My bad for leaving it around.

The comments still apply to the new PR as it was a merge-squash to make it a little cleaner.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants