-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
support for saml idp initiated flow #1514
base: master
Are you sure you want to change the base?
Conversation
Thanks! Haven't had a chance to look at this yet, but I'm excited about this. 😃 |
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.
Thanks for whipping this up. Besides my inline nitpicks and comments,
- we'll need some tests
- and some docs
- Do you think this might better be something to opt into using a config value?
I could see this as a configurable of the SAML connector, where you say that for this one, you want to allow IdP-initiated logins. Maybe even combined with client IDs? Like, for this SAML connector, client a and b can use IdP-initiated logins?
Thanks again for contributing this! 🎉
server/handlers.go
Outdated
s.renderError(w, http.StatusBadRequest, "User session error.") | ||
var code int | ||
var err error | ||
authID, samlInResponseTo, code, err = s.handleSamlCallback(r) |
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.
[style] Initialisms should be cased like this:
authID, samlInResponseTo, code, err = s.handleSamlCallback(r) | |
authID, samlInResponseTo, code, err = s.handleSAMLCallback(r) |
at least if we want to follow this -- and I think it wouldn't be bad to that. 😉
server/handlers.go
Outdated
// handleSamlCallback handles a saml callback response with support for the IdP initiated flow. | ||
// if the RelayState is not one of our auth requests, we check to see if it is a valid redirectURI | ||
// for one of our clients. then, if the SAMLResponse is from a registered connector, we create a | ||
// new auth request in the database so we can contuine with our regular callback flow. |
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.
typo:
// new auth request in the database so we can contuine with our regular callback flow. | |
// new auth request in the database so we can continue with our regular callback flow. |
server/handlers.go
Outdated
func (s *Server) handleSamlCallback(r *http.Request) (string, string, int, error) { | ||
relayState := r.PostFormValue("RelayState") | ||
if relayState == "" { | ||
return "", "", http.StatusBadRequest, fmt.Errorf("user session 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.
There's no need to format anything, let's do
return "", "", http.StatusBadRequest, fmt.Errorf("user session error") | |
return "", "", http.StatusBadRequest, errors.New("user session error") |
Also, something more helpful might be good, if we're touching this part anyways? Like, "missing RelayState"? 🤔
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.
whoops yeah can definitely use errors.New()
re: the various error message comments, i was trying to keep the errors returned to the user intentionally vague for security reasons. since this is already a flow prone to man-in-the-middle attacks i was trying to reduce information gleaned from specific error messages and provide the more detailed error in the logs.
if this isn't something we find important, i can certainly make the returned errors more specific.
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.
You're correct, better safe than sorry. We'll log the details.
server/handlers.go
Outdated
return relayState, relayState, http.StatusOK, nil | ||
} else if err != storage.ErrNotFound { | ||
s.logger.Errorf("Failed to get auth request: %v", err) | ||
return "", "", http.StatusInternalServerError, fmt.Errorf("database 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.
Like above, let's either include the err
here (fmt.Errorf("database error: %v", err)
) or use errors.New()
. (There's a few more of these below, not flagging them separately 😉)
server/handlers.go
Outdated
return "", "", http.StatusInternalServerError, fmt.Errorf("database error") | ||
} | ||
// find the client the RelayState is trying to send us to, stop when we match a redirectURI | ||
// TODO: is it valid for differrent clients to have the same redirectURI? ¯\_(ツ)_/¯ |
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 believe it is 🤔 But it might be something to call out in some docs or comments as a restriction when wanting to use IdP-initiated login.
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.
another idea i had is to put the clientId in the RelayState and provide something like a samlRedirectURI config on the client to decide which URI we should send the code to once we've verified the SAML request. this would be a more substantial change but perhaps worth it?
server/handlers.go
Outdated
} | ||
if client == nil { | ||
s.logger.Errorf("Cannot find client with redirectURI: %s", relayState) | ||
return "", "", http.StatusInternalServerError, fmt.Errorf("bad saml response") |
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.
No I believe a 400 might be OK here, too.
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.
oh yeah, agreed. copy/paste error
server/handlers.go
Outdated
var cfg saml.Config | ||
if err := json.Unmarshal(c.Config, &cfg); err != nil { | ||
s.logger.Errorf("Cannot parse saml config for connector %s: %s", c.ID, err) | ||
return "", "", http.StatusPreconditionFailed, fmt.Errorf("config 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.
Hmm not 500? 💭 But this really cannot happen.... if the config was bad, the server wouldn't start, I think.
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.
yeeeah agreed that this shouldn't ever really happened, but wanted to handle the error regardless. since it's config related i figured it made sense as a precondition failure
server/handlers.go
Outdated
State: "", | ||
Nonce: "", | ||
ForceApprovalPrompt: false, | ||
// TODO: make IdP initiated scopes configurable as part of the saml connector |
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.
👍
hey @srenatus here's a thought on supporting custom scopes rather than the hardcoded ones i've got now. update the Client struct in storage to something like... type Client struct {
...
SAMLInitiated *SAMLInitiatedConfig
}
type SAMLInitiatedConfig struct {
RedirectURI string
Scopes []string
} this way each client could have different scopes for dealing with an IdP initiated scope. this change also includes the idea i had for handling RelaySate differently were we don't have to try and match to some existing redirectURI on any client, we'd just check if the RelayState matches some existing clientID and then see if that client has this SAMLInitiatedConfig to figure out how this request is handled. finally, this means any client would need to opt-in to working with IdP initiated flows by providing this configuration. perhaps there's even additional configuration here to decide which connectors the client is willing to work with as you described in your original comment. just wanted to get some feedback on this design before implementing since it impacts storage and thus a bit less trivial to implement. |
I haven't dug into how much of an effort this is going to be, but -- taken at face value, having the clients determine if they want to allow being selected for IdP-initiated Sign In seems like a good choice 👍 My only hesitation is that we're adding something to clients that is very specific to a single type of connector. (💭 I wonder if this could be abstracted?) I mean, on the other hand, the SAML connector(s) already necessitate special handling in the server's handlers, and that's a thing differentiating SAML from the rest. (What I mean is that we've already gone down that road.) |
876b2d4
to
f1f52c4
Compare
@srenatus i just pushed some changes with an initial attempt at the approach i described. i've not updated the various storage connectors, but what i had was enough to work with the static config i've been testing with. further scope would involve updating the various storage connectors (some of which look to be easier than others to update). i feel you on being adverse to having much connector-specific config on the client, but i'm not sure of a better way to handle this.. the whole IdP initiated flow is outside of anything oauth intends to do (with good reason), so it's hard to not have SAML things creep outside of the connector. if you've got any good ideas for better abstraction let me know! and let me know what you think about the current approach with my latest commit! |
0f671ac
to
36b413e
Compare
add column saml_init_redirect_uri text not null default '';`, | ||
` | ||
alter table client | ||
add column saml_init_scopes bytea; -- JSON array of strings`, |
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.
@srenatus i took a stab at updating the storage connectors with my changes. tests are passing now, but one questionable bit is this migration. seems like the convention is to make all columns not null
across every table, but i ran into a bit of a conflict here.
postgresql won't let you add a not null
column to a table without specifying a default, however mysql won't let you specify a default on a BLOB
column (what bytea
translates to in the mysql flavor). so.... i just let this column be nullable. not sure if there's any potential issues with that.
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.
We'd expect most clients not to have a SAML config, wouldn't we? So, null seems alright for that...? 🤔
621ae04
to
9ac5a96
Compare
Sorry for radio silence -- I have not forgotten about this. (I still want this! 😄) |
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 looks pretty good. As we've discussed already, this goes pretty deep into the service, but SAML has broken the connector abstraction right from the start. (Also, IdP-initiated logins seem to be SAML-only feature, given our set of connectors, so I suppose this is OK.)
What I'd like to get in with this is, if you don't mind,
-
docs
-
some tests
Now, this doesn't have to go as far as SAML test scaffolding #1295; but maybe there must be some things we could test here...?
Also: Thank you for working on this! It's great to see this happen 🎉
server/handlers.go
Outdated
return "", "", http.StatusBadRequest, errors.New("user session error") | ||
} | ||
// TODO: should we check that 'idtoken' is one of the scopes? or just put the | ||
// burden on the user to confiugre these scopes correctly? |
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.
// burden on the user to confiugre these scopes correctly? | |
// burden on the user to configure these scopes correctly? |
storage/kubernetes/types.go
Outdated
@@ -167,6 +167,14 @@ type Client struct { | |||
|
|||
Name string `json:"name,omitempty"` | |||
LogoURL string `json:"logoURL,omitempty"` | |||
|
|||
SAMLInitiated SAMLInitiatedConfig `json:"samlInitiated"` |
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.
❓ How about adding ,omitempty
? I suspect most people won't use this right away... 🤔
add column saml_init_redirect_uri text not null default '';`, | ||
` | ||
alter table client | ||
add column saml_init_scopes bytea; -- JSON array of strings`, |
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.
We'd expect most clients not to have a SAML config, wouldn't we? So, null seems alright for that...? 🤔
alter table client | ||
add column saml_init_redirect_uri text not null default '';`, | ||
` | ||
alter table client |
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 think these could be combined into a single statement, ALTER TABLE client ADD COLUMN ..., ADD COLUMN
. Not sure if MySQL prohibits this?
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'm not sure.. tbh i just followed the structure of the refresh_token
migration above it which does three alter
s each in a separate statement.
hey @srenatus i updated with docs and fixes to some minor issues. i took a look at what might be needed to add some tests for |
@scotthew1 @srenatus |
Is there anything we can do to assist in the delivery of this feature? |
same, i would be interested in this feature as well for our organization |
First of all this branch has to be rebased on current master since some things around storage and CI have changed. If someone could give a stab at trying to resurrect this branch that would be awesome! |
9d63128
to
4a178b8
Compare
hey i just rebased the branch. in regards to what's need for a merge... i can throw some docs together in the near future, but i'm not sure about the testing question. never really got a response on that one and iirc there were some requirements on another issue. |
any plans to merge this? |
Sorry for having dropped the ball on this. Anyone listening? I'll try to re-review when this is rebased! 😃 |
@srenatus @scotthew1 I didn't rebase, but merged it with recent master. Still worked for me (tested with Okta). |
What is the status for this PR ? Any plans for moving ahead ? |
This PR is of interest to me (specifically doing provider-initiated flows from Okta to Argo CD), so I am working on resolving the merge conflicts and getting it to build and pass tests again. Since this PR is obviously from a fork that I do not own, I will open a separate PR once I have it in an appropriate state. |
This feature rebases from the current master branch to include PR dexidp#1514.
This feature rebases from the current master branch to include PR dexidp#1514.
hey @srenatus i know #1149 was closed due to security concerns, but this flow has become a hard requirement for our use case. that original issue discusses the ability to use the 'state' parameter to handle this, but i found that relying on 'RelayState' from the SAML provider was fine for our use case and don't think this approach ended up being too obtrusive. perhaps this is a bit naive and/or too specific to our use case, but i thought i'd put it out there for discussion.
a few notables: