Open rvaneerdewijk opened 3 years ago
Hi @rvaneerdewijk,
The Azure Plugin is a community contribution. Marking this as question, in case someone has familiarity with OAUTH2 in Azure
Thanks, @hosseinsh . I had no idea there was a community Azure plugin. I was just attempting to use the built-in OAUTH2 functionality. Do you know where I might find this?
This is the Azure destination plugin in Lemur https://github.com/Netflix/lemur/blob/192112f4ce735bfba0c443f7a7ee8108777f4faf/lemur/plugins/lemur_azure_dest/plugin.py#L7-L8
@hosseinsh from what I'm reading, "destination" is not what I'm looking for. I'm looking to have it so Azure AD users can log into Lemur. So far OAuth2 sounds like the closest fit but I'm not sure of the specific parameters.
Oh, sorry I got it the wrong way that you are integrating with azure to upload certificates.
Correct, Lemur does provide Oauth2 support for the Lemur login.
would you make sure to not log any secrets and redact the value from OAUTH2_SECRET.
@hosseinsh those are fake values up there ^^ so nothing to worry about. I made sure to replace them when posting.
Bumping this... anyone? Is anyone using OAUTH2 at all? And how does Lemur decide who can/can't log into Lemur and what role they belong to?
Still stuck here.
I've managed to find some more URLs related to OAUTH2 for Azure:
OAUTH2_JWKS_URL = "https://login.microsoftonline.com/common/discovery/keys" OAUTH2_USER_API_URL = "https://login.microsoftonline.com/common/openid/userinfo"
... but these change nothing. When I point to the OAUTH2 v2 URLs instead of v1, it doesn't throw an error, but it doesn't log me in either. It just sits there after authentication and there are no helpful messages in the log.
Surely somebody knows something.
These URLs show the configuration options for OAUTH2:
v1: https://login.microsoftonline.com/common/.well-known/openid-configuration v2: https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
When happens in Lemur when somebody authenticates via OAUTH2? Is a user account automatically created? Does the user need to be pre-populated? I'm not sure what Lemur expects.
Thanks
Hi @rvaneerdewijk can you try this?
https://github.com/Netflix/lemur/issues/3705#issuecomment-892675964
It's possible that Azure implemented the OAuth2 RFC in a case-sensitive way.
Hi @rvaneerdewijk can you try this?
It's possible that Azure implemented the OAuth2 RFC in a case-sensitive way.
Changing the headers line in views.py to:
"Authorization": "Basic {0}".format(basic.decode("utf-8")),
... didn't seem to solve it. But I may be missing something elsewhere.
The error seems to have changed:
2021-08-04 15:41:35,642 DEBG 'lemur-web' stdout output: [2021-08-04 15:41:35,629] ERROR in app: Exception on /api/1/auth/oauth2 [POST] Traceback (most recent call last): File "/www/lemur/lib/python3.8/site-packages/flask/app.py", line 1950, in full_dispatch_request rv = self.dispatch_request() File "/www/lemur/lib/python3.8/site-packages/flask/app.py", line 1936, in dispatch_request return self.view_functionsrule.endpoint File "/www/lemur/lib/python3.8/site-packages/flask_restful/init.py", line 467, in wrapper resp = resource(*args, kwargs) File "/www/lemur/lib/python3.8/site-packages/flask/views.py", line 89, in view return self.dispatch_request(*args, *kwargs) File "/www/lemur/lib/python3.8/site-packages/flask_restful/init.py", line 582, in dispatch_request resp = meth(args, kwargs) File "/www/lemur/lemur/auth/views.py", line 562, in post user, profile = retrieve_user(user_api_url, access_token) File "/www/lemur/lemur/auth/views.py", line 142, in retrieve_user profile = r.json() File "/www/lemur/lib/python3.8/site-packages/requests/models.py", line 900, in json return complexjson.loads(self.text, **kwargs) File "/usr/lib/python3.8/json/init.py", line 357, in loads return _default_decoder.decode(s) File "/usr/lib/python3.8/json/decoder.py", line 337, in decode obj, end = self.raw_decode(s, idx=_w(s, 0).end()) File "/usr/lib/python3.8/json/decoder.py", line 355, in raw_decode raise JSONDecodeError("Expecting value", s, err.value) from None json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
2021-08-04 15:41:35,645 DEBG 'lemur-web' stdout output: Exception on /api/1/auth/oauth2 [POST] Traceback (most recent call last): File "/www/lemur/lib/python3.8/site-packages/flask/app.py", line 1950, in full_dispatch_request rv = self.dispatch_request() File "/www/lemur/lib/python3.8/site-packages/flask/app.py", line 1936, in dispatch_request return self.view_functionsrule.endpoint File "/www/lemur/lib/python3.8/site-packages/flask_restful/init.py", line 467, in wrapper resp = resource(*args, kwargs) File "/www/lemur/lib/python3.8/site-packages/flask/views.py", line 89, in view return self.dispatch_request(*args, *kwargs) File "/www/lemur/lib/python3.8/site-packages/flask_restful/init.py", line 582, in dispatch_request resp = meth(args, kwargs) File "/www/lemur/lemur/auth/views.py", line 562, in post user, profile = retrieve_user(user_api_url, access_token) File "/www/lemur/lemur/auth/views.py", line 142, in retrieve_user profile = r.json() File "/www/lemur/lib/python3.8/site-packages/requests/models.py", line 900, in json return complexjson.loads(self.text, kwargs) File "/usr/lib/python3.8/json/init.py", line 357, in loads return _default_decoder.decode(s) File "/usr/lib/python3.8/json/decoder.py", line 337, in decode obj, end = self.raw_decode(s, idx=_w(s, 0).end()) File "/usr/lib/python3.8/json/decoder.py", line 355, in raw_decode raise JSONDecodeError("Expecting value", s, err.value) from None json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) Exception on /api/1/auth/oauth2 [POST] Traceback (most recent call last): File "/www/lemur/lib/python3.8/site-packages/flask/app.py", line 1950, in full_dispatch_request rv = self.dispatch_request() File "/www/lemur/lib/python3.8/site-packages/flask/app.py", line 1936, in dispatch_request return self.view_functions[rule.endpoint](req.view_args) File "/www/lemur/lib/python3.8/site-packages/flask_restful/init.py", line 467, in wrapper resp = resource(*args, kwargs) File "/www/lemur/lib/python3.8/site-packages/flask/views.py", line 89, in view return self.dispatch_request(*args, *kwargs) File "/www/lemur/lib/python3.8/site-packages/flask_restful/init.py", line 582, in dispatch_request resp = meth(args, kwargs) File "/www/lemur/lemur/auth/views.py", line 562, in post user, profile = retrieve_user(user_api_url, access_token) File "/www/lemur/lemur/auth/views.py", line 142, in retrieve_user profile = r.json() File "/www/lemur/lib/python3.8/site-packages/requests/models.py", line 900, in json return complexjson.loads(self.text, **kwargs) File "/usr/lib/python3.8/json/init.py", line 357, in loads return _default_decoder.decode(s) File "/usr/lib/python3.8/json/decoder.py", line 337, in decode obj, end = self.raw_decode(s, idx=_w(s, 0).end()) File "/usr/lib/python3.8/json/decoder.py", line 355, in raw_decode raise JSONDecodeError("Expecting value", s, err.value) from None json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
Hi @rvaneerdewijk - nice! That adjustment seems to have gotten you closer to the end of the OAuth2 flow. This new error is during retrieval of the user profile after the token exchange, which seems to have succeeded now. I haven't used Azure before, but maybe you want to add that authorization bearer when retrieving the user profile by ensuring that this line runs:
Not sure if that works but worth a try.
Hi @rvaneerdewijk - Did you manage to fix this problem? I am encountering similar issue when integrating with OKTA. I don't think, the request is being successfully sent to OKTA. If you have managed to fix this problem, please share the resolution.
As Sam suggested, I have added header, but it doesn' t seem to fix the problem. I tried to print the response, and looks like it is throwing 400 error. So, I am suspecting that the request is not being prepared correctly at the lemur level.
Also, I don't see any logs on the OKTA side
Hi @rvaneerdewijk - nice! That adjustment seems to have gotten you closer to the end of the OAuth2 flow. This new error is during retrieval of the user profile after the token exchange, which seems to have succeeded now. I haven't used Azure before, but maybe you want to add that authorization bearer when retrieving the user profile by ensuring that this line runs:
Not sure if that works but worth a try.
I wouldn't know how to check that. I've added "access tokens" along with "ID tokens" on the Azure side, but so far it seems to be throwing the same errors. Pretend I don't know anything about OAUTH2 or Flask development (because I don't... I'm bumbling my way through this. I struggle to read code written by "real" programmers that doesn't look like QBasic).
Hi @rvaneerdewijk - Did you manage to fix this problem? I am encountering similar issue when integrating with OKTA. I don't think, the request is being successfully sent to OKTA. If you have managed to fix this problem, please share the resolution.
As Sam suggested, I have added header, but it doesn' t seem to fix the problem. I tried to print the response, and looks like it is throwing 400 error. So, I am suspecting that the request is not being prepared correctly at the lemur level.
Also, I don't see any logs on the OKTA side
Nope, not yet. I 100% need my hand held here.
Hi @rvaneerdewijk - nice! That adjustment seems to have gotten you closer to the end of the OAuth2 flow. This new error is during retrieval of the user profile after the token exchange, which seems to have succeeded now. I haven't used Azure before, but maybe you want to add that authorization bearer when retrieving the user profile by ensuring that this line runs: https://github.com/Netflix/lemur/blob/26601920820e6138fe78b7d5fc9af18019af5e2b/lemur/auth/views.py#L134
Not sure if that works but worth a try.
I wouldn't know how to check that. I've added "access tokens" along with "ID tokens" on the Azure side, but so far it seems to be throwing the same errors. Pretend I don't know anything about OAUTH2 or Flask development (because I don't... I'm bumbling my way through this. I struggle to read code written by "real" programmers that doesn't look like QBasic).
Just having a look at this line... it looks like it is preceded by:
if current_app.config.get("PING_INCLUDE_BEARER_TOKEN"):
I don't have any PING-related configuration options set because I didn't think it would apply here. So I would think the line quoted above would not run.
I may be close to getting this. I commented out the line:
if current_app.config.get("PING_INCLUDE_BEARER_TOKEN"):
and collapsed the indent for the line immediately after it:
headers = {"Authorization": f"Bearer {access_token}"}
This changed my error to:
2021-08-10 17:05:31,237 DEBG 'lemur-web' stdout output:
Exception on /api/1/auth/oauth2 [POST]
Traceback (most recent call last):
File "/www/lemur/lib/python3.8/site-packages/flask/app.py", line 1950, in full_dispatch_request
rv = self.dispatch_request()
File "/www/lemur/lib/python3.8/site-packages/flask/app.py", line 1936, in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
File "/www/lemur/lib/python3.8/site-packages/flask_restful/__init__.py", line 467, in wrapper
resp = resource(*args, **kwargs)
File "/www/lemur/lib/python3.8/site-packages/flask/views.py", line 89, in view
return self.dispatch_request(*args, **kwargs)
File "/www/lemur/lib/python3.8/site-packages/flask_restful/__init__.py", line 582, in dispatch_request
resp = meth(*args, **kwargs)
File "/www/lemur/lemur/auth/views.py", line 562, in post
user, profile = retrieve_user(user_api_url, access_token)
File "/www/lemur/lemur/auth/views.py", line 144, in retrieve_user
user = user_service.get_by_email(profile["email"])
KeyError: 'email'
I added 'email' to the optional claims in Azure. Now my error is:
2021-08-10 17:08:55,746 DEBG 'lemur-web' stdout output:
[2021-08-10 17:08:55,744] WARNING in views: OAuth State token is too stale.
OAuth State token is too stale.
OAuth State token is too stale.
This looks a LOT better.
If I restart the Lemur service, I get the complaint about 'email' again on first login attempt, and then the state token error on subsequent attempts.
Any ideas? Am I getting closer or making it worse?
@rvaneerdewijk nice!! It looks like you're getting to the very end of the flow. For the stale OAuth token issue, that warning is intended to be thrown to prevent users from returning state tokens that are too old. Is the login with Azure taking longer than 15 seconds (the default tolerance)? Can you try setting OAUTH_STATE_TOKEN_STALE_TOLERANCE_SECONDS
in your config to something higher like 60
seconds?
Also, I would note that the state token staleness check occurs before the errors you'd seen previously. Which would indicate that you've been able to get past this error in the past.
I've tried setting the OAUTH_STATE_TOKEN_SECRET and OAUTH_STATE_TOKEN_STALE_TOLERANCE_SECONDS parameters.
I'm still ending up with the 'email' error.
The "OAuth State token is too stale" error seems to be delayed by however many seconds I put in the OAUTH_STATE_TOKEN_STALE_TOLERANCE_SECONDS parameter. Right now I'm experimenting with 60.
For the email error -- can you see what's in the full profile
dict by printing this line? The KeyError
indicates that Azure is not including any data under "email"
in its response to your /userinfo
request, but it's possible that user emails are being returned under a different key name like "mail"
or "userPrincipalName"
I've exported the contents of 'profile' by adding the following lines afterward:
with open('/tmp/profile_json.txt', 'w') as jsonfile:
jsonfile.write(json.dumps(profile))
Sure enough, "email" is not there. What is there:
aio amr family_name given_name ipaddr name oid onprem_sid rh sub tid unique_name upn uti ver
If modifying the code to use upn is easy, that's preferable and should be more consistent.
I guess that would mean modifying the OAuth scope to change "email" to "upn" and any reference to profile["email"] to profile["upn"]?
Actually, figured out a much simpler solution... followed up the line:
profile = r.json()
with:
profile["email"] = profile["upn"]
I am able to log in now!
Yay! @hosseinsh may have additional thoughts, but it could make sense to raise a PR for this to support future Azure users who want to use OAuth2. It sounds like the Azure-specific authentication changes that were needed to get Lemur working were these:
"authorization": "basic {0}"
to "Authorization": "Basic {0}"
because Azure's oauth2 implementation is case-sensitive on auth headersUSER_PROFILE
that is by default set to "email"
(this way, in lemur/auth/views.py
, profile email access for user creation would happen as profile[current_app.config.get("USER_PROFILE")]
). The config would include a comment above it about Azure needing to change it to "upn"
.AZURE_INCLUDE_BEARER_TOKEN
could make sense.Does that sound right?
Correct that all sounds right.
On the Azure side, the app registration is configured as follows...
Redirect URI = same as OAUTH2_REDIRECT_URI below
Checkboxes checked:
API Permissions:
User.Read User.ReadBasic.All
The file lemur.conf.py needs lines as follows:
OAUTH2_SECRET = '[client secrets generated in Azure' OAUTH2_ACCESS_TOKEN_URL = "https://login.microsoftonline.com/[Directory (tenant) ID]/oauth2/token" OAUTH2_NAME = "Azure" <--- name this whatever you want OAUTH2_CLIENT_ID = "[Application (client) ID]" OAUTH2_REDIRECT_URI = "https://[your Lemur hostname]/api/1/auth/oauth2" OAUTH2_AUTH_ENDPOINT = "https://login.microsoftonline.com/[Directory (tenant) ID]/oauth2/authorize" OAUTH2_JWKS_URL = "https://login.microsoftonline.com/common/discovery/keys" OAUTH2_USER_API_URL = "https://login.microsoftonline.com/[Directory (tenant) ID]/openid/userinfo" OAUTH2_VERIFY_CERT = True OAUTH_STATE_TOKEN_SECRET = b'[generated as per documentation]' OAUTH_STATE_TOKEN_STALE_TOLERANCE_SECONDS = 60 <--- this could probably be less
... replacing the parameters in square brackets.
We are currently deploying Lemur to handle our certs and ran into the same issue with OAUTH2
authentication using Azure. We deployed the Flask API and the UI separately under different domain names, so we had to point the PING_REDIRECT_URI
to the UI and the PING_URL
to the API. We were able to get everything to work using the following config:
PING_SECRET = [client secret generated in azure] PING_ACCESS_TOKEN_URL = https://login.microsoftonline.com/[tenant_id]/oauth2/v2.0/token PING_USER_API_URL = https://graph.microsoft.com/oidc/userinfo PING_JWKS_URL = https://login.microsoftonline.com/[tenant_id]/discovery/v2.0/keys PING_NAME = [lemur display name] PING_CLIENT_ID = [azure application client id] PING_URL = https://[api domain name]/api/1/auth/ping PING_REDIRECT_URI = https://[ui domain name] PING_AUTH_ENDPOINT = https://login.microsoftonline.com/[tenant_id]/oauth2/v2.0/authorize PING_INCLUDE_BEARER_TOKEN = True OAUTH_STATE_TOKEN_SECRET = b'[generated as per documentation]'
Azure Settings:
Authentication:
Select the tokens you would like to be issued by the authorization endpoint: -- Access tokens -- ID tokens
Platform configurations: -- Web - Redirect URIs = https://[ui domain name] -- Front-channel logout URL = https://[ui domain name]/#/logout
API Permissions:
NOTE: Using https://graph.microsoft.com/oidc/userinfo
for the User API URL will return the email
without having to change to upn
@havron - Thanks for posting this! We had to change the "authorization": "basic {0}"
to "Authorization": "Basic {0}"
as well.
@hosseinsh - Would like to suggest another potential PR to address the separate front-end and back-end OAUTH2
issue. We could add the following config params PING_URL
and OAUTH2_URL
. Both params could be defaulted to PING_REDIRECT_URI
and OAUTH2_REDIRECT_URI
to ensure backwards compatibility.
Changed to: "url": current_app.config.get("PING_URL", current_app.config.get("PING_REDIRECT_URI")),
Changed to: "url": current_app.config.get("OAUTH2_URL", current_app.config.get("OAUTH2_REDIRECT_URI")),
@JoeMcRobot that sounds good to me, happy to review the respective PR.
Hi, I'm struggling to get OAUTH2 authentication working with Azure. It's possible I've left out important fields or are using the wrong values.
Config file options (with values changed for security reasons):
ACTIVE_PROVIDERS = ["oauth2"] OAUTH2_SECRET = 'e0~r14.IIpG~-daPyAR2.QjsDgxaWjfo91' OAUTH2_ACCESS_TOKEN_URL = "https://login.microsoftonline.com/1ede324e-872d-4f4b-8e56-ce3ed381b23c/oauth2/token" OAUTH2_NAME = "Azure" OAUTH2_CLIENT_ID = "12354aa4-116b-4d79-9b4a-0f4d3941f87b" OAUTH2_REDIRECT_URI = "https://lemur.ourdomain.com/api/1/auth/oauth2" OAUTH2_AUTH_ENDPOINT = "https://login.microsoftonline.com/1ede324e-872d-4f4b-8e56-ce3ed381b23c/oauth2/authorize" OAUTH2_VERIFY_CERT = True
This adds an option to the login page. When I click the new button for logging into Azure, it opens up a pop-up verifying which Azure account I want to use. I select the account, and I immediately get the warning "Whoa there ... The supplied credentials are invalid". The log file has the following entries:
2021-06-30 14:43:15,772 DEBG 'lemur-web' stdout output: Exception on /api/1/auth/oauth2 [POST] Traceback (most recent call last): File "/www/lemur/lib/python3.8/site-packages/flask/app.py", line 1950, in full_dispatch_request rv = self.dispatch_request() File "/www/lemur/lib/python3.8/site-packages/flask/app.py", line 1936, in dispatch_request return self.view_functionsrule.endpoint File "/www/lemur/lib/python3.8/site-packages/flask_restful/init.py", line 467, in wrapper resp = resource(*args, kwargs) File "/www/lemur/lib/python3.8/site-packages/flask/views.py", line 89, in view return self.dispatch_request(*args, *kwargs) File "/www/lemur/lib/python3.8/site-packages/flask_restful/init.py", line 582, in dispatch_request resp = meth(args, kwargs) File "/www/lemur/lemur/auth/views.py", line 548, in post id_token, access_token = exchange_for_access_token( File "/www/lemur/lemur/auth/views.py", line 80, in exchange_for_access_token id_token = r.json()["id_token"] KeyError: 'id_token'
Is there a best practices document for setting up Lemur with Azure auth?
Thanks