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

Implement refreshing with Google #1180

Merged
merged 27 commits into from
Nov 19, 2019
Merged

Conversation

JoelSpeed
Copy link
Contributor

Fixes: #863

I've read through the document provided in this comment by @ericchiang and implemented the changes he describes.

I've been testing it with the memory storage and connecting to Google so far and everything seems to be working (Added lots of Prints while debugging).

Have had two clients set up, logged the same user into A, then into B (updating the refresh token since A's is now invalid), then refreshed client A, which used the token retrieved by client B to refresh.

I believe this should now allow the creation of a specific Google connector, in turn allowing someone to implement retrieval of groups using their Admin Directory API

@ericchiang
Copy link
Contributor

This is a big PR so I just have some overall questions before I dive into a review.

How does the migration work? If a user already has a refresh token in the system, then they upgrade dex, what happens to the connector data associated with that refresh token?

@JoelSpeed
Copy link
Contributor Author

At present, they lose it I believe.

I had a look through the code to try and mitigate that but couldn't see anything obvious.

I'm guessing if there is a solution it would be in the storage section of the codebase. Do the SQL/K8s/Etcd storage backends have migration scripts that can be amended?

} else {
connectorData = session.ConnectorData
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess to allow migration some code could be added here, checking if the ConnectorData is present, if not check refresh object for it. But this means putting the ConnectorData field back in the struct and would probably mean this migration code would be there forever.

I guess it depends how important you think maintaining existing refresh tokens is

@JoelSpeed
Copy link
Contributor Author

Did a little refactoring and built an image to make it easier for anyone wanting to test these changes: quay.io/joelspeed/dex:refresh-tokens-1

@JoelSpeed
Copy link
Contributor Author

Have just tested this with an existing SQL DB and realised the migrations didn't work quite as I thought they did. I've fixed this now and migrations from existing installations are now working. New image quay.io/joelspeed/dex:refresh-tokens-2

@JoelSpeed
Copy link
Contributor Author

There was also an error in the updater function for offline sessions in the SQL connector, this is now fixed. quay.io/joelspeed/dex:refresh-tokens-3

@jhohertz
Copy link

I'm using this now in conjunction with #1185, would love to see both mainlined. Let me know if I can help or test in any way.

@marco-m
Copy link

marco-m commented Sep 26, 2018

@ericchiang did you have a chance to review this PR ?

@JoelSpeed
Copy link
Contributor Author

Rebased onto latest master

@JoelSpeed
Copy link
Contributor Author

@ericchiang @srenatus Just been talking about this on the K8s office hours, any chance either of you have time to review?

@srenatus
Copy link
Contributor

srenatus commented Feb 21, 2019 via email

@ericchiang
Copy link
Contributor

Sorry, context switching back to this PR.

Did the migration strategy ever get worked out? I'm particularly interested because dex is horizontally scalable, so we have to tolerate multiple versions of dex running at the same time during a live migration. What happens with the ConnectorData in that case?

@tsuna
Copy link
Contributor

tsuna commented Apr 5, 2019

Is this PR still active somehow or is this effort kind of abandoned at this point? Would love to see this merged!

@JoelSpeed
Copy link
Contributor Author

I dropped the ball on this a bit, but will try and take a look at #1180 (comment) and respond within the next week, sorry for the delay

@JoelSpeed
Copy link
Contributor Author

@ericchiang I've reverted all of the changes to remove the old connector data and added some migration code in the final commit, WDYT?

I'll squash all the reverts into their original commits to clean up the history if you are happy with this migration approach

connector/oidc/oidc.go Outdated Show resolved Hide resolved
Copy link
Contributor

@ericchiang ericchiang left a comment

Choose a reason for hiding this comment

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

This should be good to merge soon.

One point though. Dex may be running in a setup where there are multiple versions of dex. If we're running vN, then upgrade to vN+1, the later version will be writing fields that vN doesn't know about (and possibly overwritting due to marshaling quirks).

Can we introduce the ConnectorData in version vN+1, but don't store any data in it until vN+2? That way by deployments run vN+2 any other versions of dex will already understand how to interpret ConnectorData.

}
} else if len(refresh.ConnectorData) > 0 {
// Use the old connector data if it exists, should be deleted once used
connectorData = session.ConnectorData
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this be refresh.ConnectorData?

@@ -224,11 +248,7 @@ func (c *oidcConnector) HandleCallback(s connector.Scopes, r *http.Request) (ide
Username: claims.Username,
Email: claims.Email,
EmailVerified: claims.EmailVerified,
ConnectorData: []byte(token.RefreshToken),
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: this should probably be a marshaled struct like the existing ConnectorData, in case we want to add additional data later.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure I follow. Isn't token.RefreshToken opaque (probably random) data? What struct would this be then? ... 💡 Oh I see.

type cd struct {
  refreshToken []byte
}
// ...
identity = connector.Identity{
		UserID:        idToken.Subject,
		Username:      claims.Username,
		Email:         claims.Email,
		EmailVerified: claims.EmailVerified,
		ConnectorData: cd{refreshToken: []byte(token.RefreshToken)},
}

....yeah I think some kind of struct would be better here. 👍

@@ -485,6 +485,41 @@ func (s *Server) finalizeLogin(identity connector.Identity, authReq storage.Auth
s.logger.Infof("login successful: connector %q, username=%q, email=%q, groups=%q",
authReq.ConnectorID, claims.Username, email, claims.Groups)

if _, ok := conn.(connector.RefreshConnector); ok {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Maybe avoid this indenting?

returnURL := path.Join(s.issuerURL.Path, "/approval") + "?req=" + authReq.ID
refreshConn, ok := conn.(connector.RefreshConnector)
if !ok {
    return returnURL, nil
}

session, err := s.storage.GetOfflineSessions(identity.UserID, authReq.ConnectorID)
if err != nil {
    if err != storage.ErrNotFound {
        // ...
    }
    if err := s.storage.CreateOfflineSessions(...) {
        // ...
    }
    return returnURL, nil
}
if err := s.storage.UpdateOfflineSessions(
...
); err != nil {
    // ...
}
return returnURL, nil

@ericchiang
Copy link
Contributor

Also please squash 😃

@dbyron0
Copy link

dbyron0 commented Jul 3, 2019

@JoelSpeed is this PR still alive? Would love to see it go in.

@JoelSpeed
Copy link
Contributor Author

@JoelSpeed is this PR still alive? Would love to see it go in.

Hey, yeah it is, sorry for the delay, hoping to get some time to work on it next week, it's been pretty low priority for me unfortunately

Can we introduce the ConnectorData in version vN+1, but don't store any data in it until vN+2? That way by deployments run vN+2 any other versions of dex will already understand how to interpret ConnectorData.

@ericchiang I'll try and create a PR to do this next week and we can go from there. Is it worth having the fallback logic so that it tries to load from the future ConnectorData if the current isn't set? Then switch that in the next version. Then theoretically both versions should be able to read the data?

Copy link
Contributor

@srenatus srenatus left a comment

Choose a reason for hiding this comment

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

This generally looks decent, I'll follow Eric in the struct comment, though, I think.

And I haven't yet had a chance to play with this.

In general, I think reviving it would be great. 👍 💯

}
token, err := c.oauth2Config.TokenSource(ctx, t).Token()
if err != nil {
return identity, fmt.Errorf("oidc: failed to get token: %v", err)
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we make this say "... refresh token..." to differentiate it from the error logged above?

@@ -224,11 +248,7 @@ func (c *oidcConnector) HandleCallback(s connector.Scopes, r *http.Request) (ide
Username: claims.Username,
Email: claims.Email,
EmailVerified: claims.EmailVerified,
ConnectorData: []byte(token.RefreshToken),
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure I follow. Isn't token.RefreshToken opaque (probably random) data? What struct would this be then? ... 💡 Oh I see.

type cd struct {
  refreshToken []byte
}
// ...
identity = connector.Identity{
		UserID:        idToken.Subject,
		Username:      claims.Username,
		Email:         claims.Email,
		EmailVerified: claims.EmailVerified,
		ConnectorData: cd{refreshToken: []byte(token.RefreshToken)},
}

....yeah I think some kind of struct would be better here. 👍

justin-slowik added a commit to justin-slowik/dex that referenced this pull request Sep 23, 2019
@JoelSpeed
Copy link
Contributor Author

@srenatus and anyone else interested, I think I've fixed this up based on the comments provided so far, it's also been rebased on master so should now be conflict free!

@JoelSpeed
Copy link
Contributor Author

@quantonganh I've rebased as requested, should be up to date with master now

@bonifaido Do you have any time to review this? I think I've addressed all of the comments made so far

I'm keen to get this done by the end of the year, I'm changing role and I'm not sure I'll have opportunity to test changes as easily from January

Copy link
Member

@bonifaido bonifaido left a comment

Choose a reason for hiding this comment

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

Great work! LGTM, I think it's time to let this in ;)

@bonifaido bonifaido merged commit b1e98d8 into dexidp:master Nov 19, 2019
@JoelSpeed
Copy link
Contributor Author

Thanks @bonifaido and everyone else who has reviewed or had intrigue in this over the last couple of years! 🎉

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.

connectors: implement refreshing with Google