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

support for saml idp initiated flow #1514

Open
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

scotthew1
Copy link
Contributor

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:

  1. this relies on the ssoIssuer config (which is otherwise optional) to match the unsolicited post from the SAML provider with the connector in the dex config.
  2. to determine the client the authorization is destined for, I match the RelayState with any redirectURI a known client. i'm not sure if it's valid for two different clients to have the same redirectURI which would make it difficult to determine which client to create authorization for.
  3. i need to fabricate an auth request, so i have to assume some set of claims. it's probably best to make this configureable, but i've just got them hardcode right now.
  4. i have the 'code' response type hardcoded as well. i'm not sure whether or not it's possible to make the implicit flow work with this setup (client would expect to validate a nonce that we can't fabricate).

@srenatus
Copy link
Contributor

srenatus commented Aug 9, 2019

Thanks! Haven't had a chance to look at this yet, but I'm excited about this. 😃

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.

Thanks for whipping this up. Besides my inline nitpicks and comments,

  1. we'll need some tests
  2. and some docs
  3. 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! 🎉

s.renderError(w, http.StatusBadRequest, "User session error.")
var code int
var err error
authID, samlInResponseTo, code, err = s.handleSamlCallback(r)
Copy link
Contributor

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:

Suggested change
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. 😉

// 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.
Copy link
Contributor

Choose a reason for hiding this comment

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

typo:

Suggested change
// 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.

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")
Copy link
Contributor

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

Suggested change
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"? 🤔

Copy link
Contributor Author

@scotthew1 scotthew1 Aug 9, 2019

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.

Copy link
Contributor

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.

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")
Copy link
Contributor

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

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? ¯\_(ツ)_/¯
Copy link
Contributor

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.

Copy link
Contributor Author

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?

}
if client == nil {
s.logger.Errorf("Cannot find client with redirectURI: %s", relayState)
return "", "", http.StatusInternalServerError, fmt.Errorf("bad saml response")
Copy link
Contributor

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.

Copy link
Contributor Author

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

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")
Copy link
Contributor

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.

Copy link
Contributor Author

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

State: "",
Nonce: "",
ForceApprovalPrompt: false,
// TODO: make IdP initiated scopes configurable as part of the saml connector
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

connector/saml/saml.go Show resolved Hide resolved
@scotthew1
Copy link
Contributor Author

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.

@srenatus
Copy link
Contributor

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

@scotthew1
Copy link
Contributor Author

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

@scotthew1 scotthew1 force-pushed the saml-idp-initiated branch 3 times, most recently from 0f671ac to 36b413e Compare August 20, 2019 15:44
add column saml_init_redirect_uri text not null default '';`,
`
alter table client
add column saml_init_scopes bytea; -- JSON array of strings`,
Copy link
Contributor Author

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.

Copy link
Contributor

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

@scotthew1 scotthew1 force-pushed the saml-idp-initiated branch 2 times, most recently from 621ae04 to 9ac5a96 Compare August 22, 2019 14:49
@srenatus
Copy link
Contributor

Sorry for radio silence -- I have not forgotten about this. (I still want this! 😄)
Will review soonish.

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

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?
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// burden on the user to confiugre these scopes correctly?
// burden on the user to configure these scopes correctly?

@@ -167,6 +167,14 @@ type Client struct {

Name string `json:"name,omitempty"`
LogoURL string `json:"logoURL,omitempty"`

SAMLInitiated SAMLInitiatedConfig `json:"samlInitiated"`
Copy link
Contributor

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`,
Copy link
Contributor

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
Copy link
Contributor

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?

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'm not sure.. tbh i just followed the structure of the refresh_token migration above it which does three alters each in a separate statement.

@scotthew1
Copy link
Contributor Author

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 handleSAMLCallback() and it seems kinda difficult to do much without addressing #1295. TestHandleInvalidSAMLCallbacks in handlers_test.go already covers some of the negative testing, but adding more would require a more elaborate saml connector setup with the test server.

@pschaumburg
Copy link

@scotthew1 @srenatus
Is there any update on this? :)

@mhaddon
Copy link

mhaddon commented Mar 18, 2020

Is there anything we can do to assist in the delivery of this feature?

@Saqibm128
Copy link

same, i would be interested in this feature as well for our organization

@bonifaido
Copy link
Member

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!

@scotthew1 scotthew1 force-pushed the saml-idp-initiated branch from 9d63128 to 4a178b8 Compare March 25, 2020 15:43
@scotthew1
Copy link
Contributor Author

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.

@ghost
Copy link

ghost commented Aug 14, 2020

any plans to merge this?

@srenatus
Copy link
Contributor

Sorry for having dropped the ball on this. Anyone listening? I'll try to re-review when this is rebased! 😃

@heidemn-faro
Copy link
Contributor

@srenatus @scotthew1 I didn't rebase, but merged it with recent master. Still worked for me (tested with Okta).
Feel free to continue. Our company will most likely not need this, so I'm currently not motivated to continue myself.
faro-oss#22
https://github.com/faro-oss/dex/tree/saml-idp-initiated

@karaimin
Copy link

What is the status for this PR ? Any plans for moving ahead ?

@erhudy
Copy link

erhudy commented Nov 27, 2023

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.

MostafaAmer2200 pushed a commit to MostafaAmer2200/dex that referenced this pull request Dec 28, 2023
This feature rebases from the current master branch
to include PR dexidp#1514.
MostafaAmer2200 pushed a commit to MostafaAmer2200/dex that referenced this pull request Dec 28, 2023
This feature rebases from the current master branch
to include PR dexidp#1514.
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.

9 participants