why okta does not redirect back to service provider in SSO case - single-sign-on

i am using open source project https://github.com/jpf/okta-pysaml2-example for learning okta, saml, sso and flask. i have made the required app integration setting in okta. When i start flast web server as service provider and access http://localhost:5000 using a ngrok, i get redirected to okta login page, where i fillup user/pass. on click submit button , okta lands on https://dev-023456-admin.okta.com/admin/getting-started. however, ideally okta should redirect back to the service provider. what is wrong here? i have configured metadata properly on both the sides. what could be the problem here? how can i narrow down the issue?
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="http://www.okta.com/abcdefghijklm">
<md:IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIIDqDCCApCgAwIBAgIGAX2Vjf7/MA0GCSqGSIb3DQEBCwUAMIGUMQswCQYDVQQGEwJVUzETMBEG A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU MBIGA1UECwwLU1NPUHJvdmlkZXIxFTATBgNVBAMMDGRldi0wMjg4NzY4NjEcMBoGCSqGSIb3DQEJ ARYNaW5mb0Bva3RhLmNvbTAeFw0yMTEyMDcxNTM5NTBaFw0zMTEyMDcxNTQwNTBaMIGUMQswCQYD VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsG A1UECgwET2t0YTEUMBIGA1UECwwLU1NPUHJvdmlkZXIxFTATBgNVBAMMDGRldi0wMjg4NzY4NjEc MBoGCSqGSIb3DQEJARYNaW5mb0Bva3RhLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBAIY6Ek4yvOB3pF4+IwAgVYyxdwOXi4AoorFUmgJ1Sv8kLc6SSApwIEKj2N/F8tYDxZKEGcV7 jap8gdYZcLpm4rKfNGw0jjQ+mCT4fcUdzLTsXQ1vzTy+KILvi1Ki3cgzEKt+Oir106XKDCgp+BGc PgGKRQQroxqrUWX12yEGyZ4D7pLt5Pg4SBC9o1xRfP+hK82ev1eO+hmp39pl6QDKPG0tV6FJI/SX bnchBNpGxT2qcMO8LzudQBS9fT7Nu0WPJGRRF/zQdB+ScHvBFnjlTKjEYPwM2yNx5FYMfcZM/qZH jyAocVdAR4LMEOi1jZwlPIgESIyCXYFVptoqm1U+Lh0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA c8zc0J0p/oq8zHfVobh0QNov4NUS81iDl0ZcRC3LDJ50X360NPM3q7JRfwVMGGUWs0WNm+km0poa XYzWnZUYbRbeuvXB/lyCjUEVneF3quTpzifffVDO8sl1qosEOFlOPf1NOMiVMwRyBHqWRsbL96E1 TVBK7ezaIa/okXiPm0QflMHV9Sg1HyQHvAxo/topQI3FzrZRehrjol0K0vg29ZiV3Y32Ym2mAWD1 3fV7Fy8QcrbQpZseojkUqqfcIaCAOUtiK+oK86yDTkd6C6Ed5Ib0K7Ckwr0FdZ6w2wo4u+UIAnK+ IOOerfagyzpaT0lAPJ4G55sVX5min7plRDfqfA==</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://dev-02887686.okta.com/app/dev-02887686_testapp_1/abcdefghijklm/sso/saml"/>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://dev-02887686.okta.com/app/dev-02887686_testapp_1/abcdefghijklm/sso/saml"/>
</md:IDPSSODescriptor>
</md:EntityDescriptor>
sp side config
'service': {
'sp': {
'endpoints': {
'assertion_consumer_service': [
("http://example123.loca.lt/saml/sso/example-okta-com", BINDING_HTTP_POST),
("https://example123.loca.lt/saml/sso/example-okta-com", BINDING_HTTP_POST),
("http://example123.loca.lt/saml/sso/example-okta-com", BINDING_HTTP_REDIRECT),
("https://example123.loca.lt/saml/sso/example-okta-com", BINDING_HTTP_REDIRECT)
],
"single_logout_service": [
("http://example123.loca.lt/saml/slo/okta", BINDING_HTTP_REDIRECT),
("http://example123.loca.lt/saml/slo/okta", BINDING_HTTP_POST),
],
},
"required_attributes": ['displayName', 'mail', ],
"metadata_key_usage": "both",
"enc_cert": "use",
'allow_unsolicited': True,
'authn_requests_signed': False,
'logout_requests_signed': False,
'want_assertions_signed': True,
'want_response_signed': False,
},
},
"key_file": "./pki/mykey.pem",
"cert_file": "./pki/mycert.pem",
"xmlsec_binary": '/usr/bin/xmlsec1',
'encryption_keypairs': [
{
'key_file': "./pki/mykey.pem",
'cert_file': "./pki/mycert.pem",
},
],
setting in okta
Single Sign On URL http://example123.loca.lt/saml/sso/example-okta-com
Recipient URL http://example123.loca.lt/saml/sso/example-okta-com
Destination URL http://example123.loca.lt/saml/sso/example-okta-com
Audience Restriction http://example123.loca.lt/saml/sso/example-okta-com
service provider python fastapi code
metadata_url_for = {
# you can have multiple 'Application Integration' settings in okta and can use here for testing.
# key : value (metadata link for 'Application Integration' created in okta)
'app_integration': 'https://dev-02887686.okta.com/app/exk2zsnf574m2JD4w5d7/sso/saml/metadata'
#'app_integration': 'https://dev-02887686.okta.com/app/dev-02887686_testapp_1/exk2zsnf574m2JD4w5d7/sso/saml'
}
# get saml client configuration for a given idP name. Here SAML client means
# Service Provider.
def saml_client_for(idp_name=None):
'''
Given the name of an IdP, returns a saml configuration.
The configuration is a hash for use by saml2.config.Config
'''
if idp_name not in metadata_url_for:
raise Exception("Settings for IDP '{}' not found".format(idp_name))
rv = requests.get(metadata_url_for[idp_name]) # make http get request with given url and read its response in 'rv'
settings = {
'entityid': 'http://example123.loca.lt/saml/sso/example-okta-com',
'metadata': {
'inline': [rv.text],
},
'service': {
'sp': {
'endpoints': {
'assertion_consumer_service': [
("http://example123.loca.lt/saml/sso/example-okta-com", BINDING_HTTP_POST),
("https://example123.loca.lt/saml/sso/example-okta-com", BINDING_HTTP_POST)
],
"single_logout_service": [
("http://example123.loca.lt/saml/slo/okta", BINDING_HTTP_REDIRECT),
("http://example123.loca.lt/saml/slo/okta", BINDING_HTTP_POST),
],
},
"required_attributes": ['displayName', 'mail', ],
"metadata_key_usage": "both",
"enc_cert": "use",
'allow_unsolicited': True,
'authn_requests_signed': False,
'logout_requests_signed': False,
'want_assertions_signed': True,
'want_response_signed': False,
},
},
"key_file": "./pki/mykey.pem",
"cert_file": "./pki/mycert.pem",
"xmlsec_binary": '/usr/bin/xmlsec1',
'encryption_keypairs': [
{
'key_file': "./pki/mykey.pem",
'cert_file': "./pki/mycert.pem",
},
],
}
spConfig = Saml2Config()
spConfig.load(settings)
spConfig.allow_unknown_attributes = True
saml_client = Saml2Client(config=spConfig)
return saml_client
#app.post("/saml/sso/example-okta-com")
async def idp_initiated(request: Request):
print("In idp_initiated....\n")
key_str = 'app_integration'
saml_client = saml_client_for(key_str)
form = await request.form()
authn_response = saml_client.parse_authn_request_response(
form.get('SAMLResponse'),
entity.BINDING_HTTP_POST)
authn_response.get_identity()
user = User(authn_response.name_id, authn_response.ava, authn_response)
request.session["user_name_id"] = str(user.name_id)
user_store = {'first_name': authn_response.ava['displayName'][0], 'last_name': authn_response.ava['displayName'][0],
'user_name': authn_response.ava['cn'][0]}
# instead of responding with user data. We are redirecting it to home page.
# resp = JSONResponse(user_store)
resp = RedirectResponse("https://example123.com/home")
uid = rndstr(32)
uid2user[uid] = user
resp.set_cookie(key='spauthn', value=authn_response.name_id, httponly=True, expires=1800)
resp.status_code = 302
return resp
#app.get("/saml/login/example-okta-com")
def sp_initiated():
print("In sp_initiated....\n")
key_str = 'app_integration'
saml_client = saml_client_for(key_str)
reqid, info = saml_client.prepare_for_authenticate()
redirect_url = None
# Select the IdP URL to send the AuthN request to
for key, value in info['headers']:
if key == 'Location':
redirect_url = value
response = fastapi.responses.RedirectResponse(redirect_url)
response.headers['Cache-Control'] = 'no-cache, no-store'
response.headers['Pragma'] = 'no-cache'
return response
then i land to

You are using metadata for idp flow. In idp flow you don't get redirected to SP this way. Use metadata for Sp flow instead which should contain the redirect url pointing to your service so idp can redirect

Related

POST data to Google Sheet web app from AWS Lambda

CURRENTLY
I have a Google Sheets App Script 'web app'
Script in Goolge Sheets
function doPost(e) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("Sheet1");
sheet.getRange("A1").setValue("Hello!")
return "Success!"
}
Google Apps Script Web App Config:
Execute as: Me // or as User. I've tried both.
Who has access: Anyone within MyOrganisation
I want to make a POST request to the above Web App from AWS Lambda.
AWS Lambda .js:
const { GoogleSpreadsheet } = require("google-spreadsheet");
const doc = new GoogleSpreadsheet(
{spreadsheetId}
);
await doc.useServiceAccountAuth({
client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
private_key: process.env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, "\n"),
});
let token = doc["jwtClient"]["credentials"]["access_token"];
await new Promise((resolve, reject) => {
const options = {
host: 'script.google.com',
path: "/macros/s/{myscriptid}/exec", //<-- my web app path!
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': "Bearer "+ token
}
};
//create the request object with the callback with the result
const req = HTTPS.request(options, (res) => {
resolve(JSON.stringify(res.statusCode));
});
// handle the possible errors
req.on('error', (e) => {
reject(e.message);
});
//do the request
req.write(JSON.stringify(data));
//finish the request
req.end();
});
console.log("response:"+JSON.stringify(response))
GCP Service Account
I have a GCP Service Account, with permission to Google Sheets API, and otherwise unrestricted access.
This Service account has EDIT access to the Google Sheet with the doPost(e) script.
Token Output:
"jwtClient": {
"_events": {},
"_eventsCount": 0,
"transporter": {},
"credentials": {
"access_token": "somelongvalue...............", //<-- what I use
"token_type": "Bearer",
"expiry_date": 1661662492000,
"refresh_token": "jwt-placeholder"
},
"certificateCache": {},
"certificateExpiry": null,
"certificateCacheFormat": "PEM",
"refreshTokenPromises": {},
"eagerRefreshThresholdMillis": 300000,
"forceRefreshOnFailure": false,
"email": "serviceaccount#appspot.gserviceaccount.com",
"key": "-----BEGIN PRIVATE KEY-----\nsomelongvalue=\n-----END PRIVATE KEY-----\n",
"scopes": [
"https://www.googleapis.com/auth/spreadsheets"
],
"subject": null,
"gtoken": {
"key": "-----BEGIN PRIVATE KEY-----\nsomelongvalue=\n-----END PRIVATE KEY-----\n",
"rawToken": {
"access_token": "somelongvalue...............",
"expires_in": 3599,
"token_type": "Bearer"
},
"iss": "serviceaccount#appspot.gserviceaccount.com",
"sub": null,
"scope": "https://www.googleapis.com/auth/spreadsheets",
"expiresAt": 1661662492000
}
}
ISSUE
Current response:
response:"401"
I cannot find any Google documentation on how to setup the headers to authenticate a request (from my service account) to my organisation restricted web app.
When the Web App is open to "Anyone" then it runs fine, but as soon as I restrict to MyOrganisation, I struggle to find a way to authenticate my POST request.
HELP!
How do I set up a POST request to my Google Sheets web app such that it can be protected by authentication? Right now, I'd be happy to find ANY means to authenticate this request (not necessarily a service account) that doesn't leave it completed open to public.
Should I use this hack?
One idea I had was to put a "secret" into my lambda function, and then make the web app public. The web app would check the secret, if if matched, would execute the function.
Modification points:
In order to access Web Apps using the access token with a script, the scopes of Drive API are required to be included. Those are https://www.googleapis.com/auth/drive.readonly, https://www.googleapis.com/auth/drive, and so on. Ref
When I saw your showing script, it seems that the access token is retrieved using google-spreadsheet. When I saw the script of google-spreadsheet, it seems that this uses only the scope of https://www.googleapis.com/auth/spreadsheets. Ref
From this situation, I thought that the reason for your current issue might be due to this. If my understanding is correct, how about the following modification? In this modification, the access token is retrieved by googleapis for Node.js from the service account. Ref
Modified script:
Google Apps Script side:
function doPost(e) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("Sheet1");
sheet.getRange("A1").setValue("Hello!")
return ContentService.createTextOutput("Success!"); // Modified
}
When you modified the Google Apps Script, please modify the deployment as a new version. By this, the modified script is reflected in Web Apps. Please be careful about this.
You can see the detail of this in the report "Redeploying Web Apps without Changing URL of Web Apps for new IDE".
Node.js side:
const { google } = require("googleapis");
const HTTPS = require("https");
const auth = new google.auth.JWT(
"###", // Please set client_email here.
null,
"###", // Please set private_key here. When you set private_key of service account, please include \n.
["https://www.googleapis.com/auth/drive.readonly"],
null
);
function req(token) {
return new Promise((resolve, reject) => {
const data = { key1: "value1" }; // Please set your value.
const options = {
host: "script.google.com",
path: "/macros/s/{myscriptid}/exec", //<-- my web app path!
method: "POST",
headers: {Authorization: "Bearer " + token},
};
const req = HTTPS.request(options, (res) => {
if (res.statusCode == 302) {
HTTPS.get(res.headers.location, (res) => {
if (res.statusCode == 200) {
res.setEncoding("utf8");
res.on("data", (r) => resolve(r));
}
});
} else {
res.setEncoding("utf8");
res.on("data", (r) => resolve(r));
}
});
req.on("error", (e) => reject(e.message));
req.write(JSON.stringify(data));
req.end();
});
}
auth.getAccessToken().then(({ token }) => {
req(token).then((e) => console.log(e)).catch((e) => console.log(e));
});
When this script is run, when the Web Apps is correctly deployed, the script of Web Apps is run and Success! is returned.
Note:
If this modified script was not useful for your Web Apps setting, please test as follows.
Please confirm whether your service account can access to the Spreadsheet again.
Please share the email address of the service account on the Spreadsheet. From your showing Google Apps Script, I thought that your Google Apps Script is the container-bound script of the Spreadsheet.
Please reflect the latest script to the Web Apps.
When you modified the Google Apps Script, please modify the deployment as a new version. By this, the modified script is reflected in Web Apps. Please be careful about this.
You can see the detail of this in the report "Redeploying Web Apps without Changing URL of Web Apps for new IDE".
When you set private_key of service account, please include \n.
References:
Web Apps
Taking advantage of Web Apps with Google Apps Script
Added:
When you will directly put the value to the Spreadsheet using Sheets API with google-spreadsheet module, you can also use the following script.
const { GoogleSpreadsheet } = require("google-spreadsheet");
const sample = async () => {
const doc = new GoogleSpreadsheet("###"); // Please set your Spreadsheet ID.
await doc.useServiceAccountAuth({
client_email: client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
private_key: process.env.GOOGLE_PRIVATE_KEY,
});
await doc.loadInfo();
const sheet = doc.sheetsByTitle["Sheet1"];
await sheet.loadCells("A1");
sheet.getCell(0, 0).value = "Hello!";
await sheet.saveUpdatedCells();
};
sample();
In this case, your service account is required to be able to access to the Spreadsheet. Please be careful about this.

How to configure Keycloak for Superset with Authlib?

I'm trying to use Authlib to setup Keycloak as SSO for Superset. Everything works fine up until when user is redirected back to Superset. Then this error occured:
authlib.integrations.base_client.errors.MismatchingStateError: mismatching_state: CSRF Warning! State not equal in request and response.
Here's my code:
In superset_config.py
AUTH_TYPE = AUTH_OAUTH
AUTH_USER_REGISTRATION = True
AUTH_USER_REGISTRATION_ROLE = 'Public'
CSRF_ENABLED = True
ENABLE_PROXY_FIX = True
OAUTH_PROVIDERS = [
{
'name': 'keycloak',
'token_key': 'access_token',
'icon': 'fa-icon',
'remote_app': {
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'client_kwargs': {
'scope': 'openid email profile'
},
'access_token_method': 'POST',
'api_base_url': 'https://KEYCLOAK_URL/auth/realms/REALM_NAME/protocol/openid-connect/',
'access_token_url': 'https://KEYCLOAK_URL/auth/realms/REALM_NAME/protocol/openid-connect/token',
'authorize_url': 'https://KEYCLOAK_URL/auth/realms/REALM_NAME/protocol/openid-connect/auth',
},
}
]
CUSTOM_SECURITY_MANAGER = OIDCSecurityManager
Here's my OIDCSecurityManager:
class OIDCSecurityManager(SupersetSecurityManager):
def get_oauth_user_info(self, provider, response=None):
if provider == 'keycloak':
me = self.appbuilder.sm.oauth_remotes[provider].get("userinfo")
return {
"first_name": me.data.get("given_name", ""),
"last_name": me.data.get("family_name", ""),
"email": me.data.get("email", "")
}
What can I do to resolve this problem? (this happens on all browser chrome, firefox, etc.)
I also had trouble with the OIDC configs, but this security manager configuration works for me.
Note - I've added roles to my Client configuration in Keycloak, and a mapper so the roles can be picked from the user info response.
class CustomSsoSecurityManager(SupersetSecurityManager):
def oauth_user_info(self, provider, response=None):
logging.debug("Oauth2 provider: {0}.".format(provider))
logging.debug("Oauth2 oauth_remotes provider: {0}.".format(self.appbuilder.sm.oauth_remotes[provider]))
if provider == 'keycloak':
# Get the user info using the access token
res = self.appbuilder.sm.oauth_remotes[provider].get(os.getenv('KEYCLOAK_BASE_URL') + '/userinfo')
logger.info(f"userinfo response:")
for attr, value in vars(res).items():
print(attr, '=', value)
if res.status_code != 200:
logger.error('Failed to obtain user info: %s', res._content)
return
#dict_str = res._content.decode("UTF-8")
me = json.loads(res._content)
logger.debug(" user_data: %s", me)
return {
'username' : me['preferred_username'],
'name' : me['name'],
'email' : me['email'],
'first_name': me['given_name'],
'last_name': me['family_name'],
'roles': me['roles'],
'is_active': True,
}
def auth_user_oauth(self, userinfo):
user = super(CustomSsoSecurityManager, self).auth_user_oauth(userinfo)
roles = [self.find_role(x) for x in userinfo['roles']]
roles = [x for x in roles if x is not None]
user.roles = roles
logger.debug(' Update <User: %s> role to %s', user.username, roles)
self.update_user(user) # update user roles
return user

Why is IdentityServer redirecting to http rather than https?

I have a very simple MVC5 website that I'm trying to secure with IdentityServer3.
Both my website and my IdentityServer instance are hosted as separate sites in AppHarbor. Both are behind https.
When I hit a resource in my website that is protected by an [Authorize] attribute (e.g., /Home/About), I am successfully redirected to IdentityServer, and I can successfully authenticate.
When IdentityServer POSTs its response back to the website (via app.FormPostResponse.js), the website responds with a 302 redirect to the requested resource - as expected. However, this redirect is to http, not https (see the network trace below).
I'm sure this is just something wrong with my IdentityServer config, but I'd appreciate any pointers as to what I've got wrong.
(AppHarbor uses a reverse proxy (nginx I believe) in front of IIS, where SSL terminates - so I have RequireSsl = false for this scenario, as per the IdentityServer documentation.)
Here is my website's Startup.cs
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://<my-idsrv3>.apphb.com/identity",
ClientId = "<my-client-id>",
Scope = "openid profile roles email",
RedirectUri = "https://<my-website>.apphb.com",
ResponseType = "id_token",
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false
});
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
}
}
Here is Startup.cs from my IdentityServer3 instance:
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.Map("/identity", idsrvApp =>
{
idsrvApp.UseIdentityServer(new IdentityServerOptions
{
SiteName = "My Identity Server",
SigningCertificate = Certificates.LoadSigningCertificate(),
RequireSsl = false,
PublicOrigin = "https://<my-idsrv3>.apphb.com",
Factory = new IdentityServerServiceFactory()
.UseInMemoryUsers(Users.Get())
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get())
});
});
}
}
Here is the definition of my website Client:
new Client
{
Enabled = true,
ClientName = "My Website Client",
ClientId = "<my-client-id>",
Flow = Flows.Implicit,
RedirectUris = new List<string>
{
"https://<my-website>.apphb.com"
},
AllowAccessToAllScopes = true
}
Here is the trace from Chrome, after clicking 'Yes, Allow' on the IdentityServer consent screen:
So it looks like this issue was caused by my client website being behind an SSL-terminating nginx front-end.
With reference to this GitHub issue, I added the following to the start of my website's app configuration:
app.Use(async (ctx, next) =>
{
string proto = ctx.Request.Headers.Get("X-Forwarded-Proto");
if (!string.IsNullOrEmpty(proto))
{
ctx.Request.Scheme = proto;
}
await next();
});
This makes the website aware that incoming requests were over https; this in turn appears to ensure that the IdentityServer3 middleware generates https uri's.
Had the same issue running identityserver4 in an Azure App Service. Even with forced https, the generated urls in .well-known/openid-configuration were still http://.
Fixed using the same solution as the other answer, but using AspNetCore ForwardedHeadersExtensions:
var forwardOptions = new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
// Needed because of mixing http and https.
RequireHeaderSymmetry = false,
};
// Accept X-Forwarded-* headers from all sources.
forwardOptions.KnownNetworks.Clear();
forwardOptions.KnownProxies.Clear();
app.UseForwardedHeaders(forwardOptions);
See also https://github.com/IdentityServer/IdentityServer4/issues/1331 for more discussion on this subject.
Add forwarded headers in your startup
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
});
and
app.UseForwardedHeaders(new ForwardedHeadersOptions()
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
Finally tell the config it has to replace the http to https in the redirect url. I'm still looking for a better way to implement this.
in your .addopenidconnect() add:
Func<RedirectContext, Task> redirectToIdentityProvider = (ctx) =>
{
if (!ctx.ProtocolMessage.RedirectUri.StartsWith("https") && !ctx.ProtocolMessage.RedirectUri.Contains("localhost"))
ctx.ProtocolMessage.RedirectUri = ctx.ProtocolMessage.RedirectUri.Replace("http", "https");
return Task.FromResult(0);
};
opt.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = redirectToIdentityProvider
};

Grails Spring Security REST Getting 403 Forbidden

I have a Grails app and I use Spring Security Plugin and the Spring Security REST Plugin as well. Normal login works well and I get a Token in the response if my credentials are correct. Now I want to access a controller and I do pass the token in the header but I get a 403 forbidden response. The Authentication works I guess because when I change the Token I get a 401 not authorized.
config.groovy
// Added by the Spring Security Core plugin:
grails.plugin.springsecurity.userLookup.userDomainClassName = 'usermanagement.User'
grails.plugin.springsecurity.userLookup.authorityJoinClassName = 'usermanagement.UserRole'
grails.plugin.springsecurity.authority.className = 'usermanagement.Role'
grails.plugin.springsecurity.controllerAnnotations.staticRules = [
'/': ['permitAll'],
'/index': ['permitAll'],
'/index.gsp': ['permitAll'],
'/assets/**': ['permitAll'],
'/**/js/**': ['permitAll'],
'/**/css/**': ['permitAll'],
'/**/images/**': ['permitAll'],
'/**/favicon.ico': ['permitAll']
]
grails.plugin.springsecurity.filterChain.chainMap = [
'/api/**': 'JOINED_FILTERS,-exceptionTranslationFilter,-authenticationProcessingFilter,-securityContextPersistenceFilter,-rememberMeAuthenticationFilter', // Stateless chain
'/**': 'JOINED_FILTERS,-restTokenValidationFilter,-restExceptionTranslationFilter' // Traditional chain
]
grails.plugin.springsecurity.rest.login.active = true
grails.plugin.springsecurity.rest.login.endpointUrl = '/api/login'
grails.plugin.springsecurity.rest.login.failureStatusCode = 401
grails.plugin.springsecurity.rest.login.useJsonCredentials = true
grails.plugin.springsecurity.rest.login.usernamePropertyName = "username"
grails.plugin.springsecurity.rest.login.passwordPropertyName = "password"
grails.plugin.springsecurity.rest.logout.endpointUrl = '/api/logout'
grails.plugin.springsecurity.rest.token.validation.activated = true
grails.plugin.springsecurity.rest.token.validation.headerName = 'X-Auth-Token'
grails.plugin.springsecurity.rest.token.storage.useGorm = true
grails.plugin.springsecurity.rest.token.storage.gorm.tokenDomainClassName = 'rest.auth.AuthenticationToken'
grails.plugin.springsecurity.rest.token.storage.gorm.tokenValuePropertyName = 'tokenValue'
grails.plugin.springsecurity.rest.token.storage.gorm.usernamePropertyName = 'username'
DailyBookingRESTController.groovy
import grails.plugin.springsecurity.annotation.Secured
#Secured(['permitAll'])
class DailyBookingRESTController {
def index() {
render "hi"
}
}
urlMapping:
class UrlMappings {
static mappings = {
"/$controller/$action?/$id?(.$format)?"{
constraints {
// apply constraints here
}
}
"/$controller/$action?/$id?(.$format)?"()
"/"(view:"/index")
"500"(view:'/error')
"/api/dailyBookings"(resources: "dailyBookingREST")
}
}
I appreciate any help!
Well as it turns out, the urlMapping is casesensitive and while i used the right url in my request when security was disabled but the wrong one when it wasn't.
I face this issues once and later realized i had to define my intercept url map pattern for refresh token url.
So from your application .groovy file go to interceptUrlMap=[] and specify the endpoint access level.
If you specified ROLE_ANONYMOUS be sure the user refreshing the token has that role, otherwise permitAll on that end point.
In actual sense, you need to specify the roles that can access that endpoint, eg: ROLE_USER, ROLE_ADMIN.....

ASP.NET MVC5 OWIN: Why User.Identity.IsAuthenticated == false after signing in via Facebook?

I am following the example of the VS2013 SPA Template - however am not using Bearer Tokens (this maybe the problem but would like to get this working just with cookies if possible).
Here is a cut down version of the relevant action method in my API Controller:
[HttpGet]
[AllowAnonymous]
public async Task<IHttpActionResult> ExternalLogin(string provider, string error = null)
{
if (!User.Identity.IsAuthenticated)
{
return new ChallengeResult(provider, this);
}
ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
// ...do stuff with externalLogin data
}
Here is the basic flow of requests:
Users clicks Facebook button, sends GET to /api/externallogin?provider=Facebook
User.Identity.IsAuthenticated returns false -> results in 401 being returned and middleware converts that into a 302 with the 'Location' header set to the facebook login page
Browser goes to the facebook login page
(https://www.facebook.com/dialog/oauth?response_type=code&client_id={myClientId}&redirect_uri=http%3A%2F%2Flocalhost%3A54819%2Fsignin-facebook&scope=user_birthday&state={someStateCode})
User signs in via Facebook -> results in browser making a call to the 'redirect_uri' but now with 'code' and 'state' parameters in the query string i.e. http://localhost:54819/signin-facebook?code={someCode}&state={someStateCode}
Response from the call to the 'redirect_uri' is a 302 with Location header set back to my application and also contains two 'Set-Cookie' headers:
Location: http://localhost:54819/api/en-gb/account/externallogin?provider=Facebook
Set-Cookie: .AspNet.Correlation.Facebook=; expires=Thu, 01-Jan-1970 00:00:00 GMT
Set-Cookie: .AspNet.ExternalCookie=Rv01CHd2onYtN_MHw2Bt71JSaOP71uRk7AP6kSilnAg7djXMh5fbZxlRCPuZhy8inhEF7ChNB261WVU3LGDuIaQmMXgz7tqXeNI-ji8qQFi2d64a720PbRVpWnkuHm2m8L87fkJAGQMJOku5gMrc0EZJfKNgjXiLv-c6Vo7PEzNch-CqcCPFHP0KBo7tGhDTbgJt-RvTzkkB1NL2JBc23eiaeda70oAW4P0NfIyj_i9mLUexHXz8Qooy9CBoLrN7Z198H_cawBfiMMF0tK1YFee2eH_TQxdmdKkUFVRz58EeIKyKUEEDswbQA9evPEHpD8BIlJPXi6R2scC44_INufXuKjHOt7LW3-sPRkUGbEWCWOn4d1B4FkHR_xOHtRpGpIdZU14xJLLiyFYKR0XxJiRlRIph8KKYnZHy61wMOl2yznOFqq3rzHOGhZ1xXEKmUlByiawPbNpdS9pNZVSHlGMbiz0FsOTf4_EVAKEXRQyxEbYjBBXD_5Ne6f7SpBqE; path=/; HttpOnly
Browser then sends GET request to the URL in the Location header from step 5 (back to my application), with the following cookie (as per the above 'Set-Cookie' directive):
Cookie: .AspNet.ExternalCookie=Rv01CHd2onYtN_MHw2Bt71JSaOP71uRk7AP6kSilnAg7djXMh5fbZxlRCPuZhy8inhEF7ChNB261WVU3LGDuIaQmMXgz7tqXeNI-ji8qQFi2d64a720PbRVpWnkuHm2m8L87fkJAGQMJOku5gMrc0EZJfKNgjXiLv-c6Vo7PEzNch-CqcCPFHP0KBo7tGhDTbgJt-RvTzkkB1NL2JBc23eiaeda70oAW4P0NfIyj_i9mLUexHXz8Qooy9CBoLrN7Z198H_cawBfiMMF0tK1YFee2eH_TQxdmdKkUFVRz58EeIKyKUEEDswbQA9evPEHpD8BIlJPXi6R2scC44_INufXuKjHOt7LW3-sPRkUGbEWCWOn4d1B4FkHR_xOHtRpGpIdZU14xJLLiyFYKR0XxJiRlRIph8KKYnZHy61wMOl2yznOFqq3rzHOGhZ1xXEKmUlByiawPbNpdS9pNZVSHlGMbiz0FsOTf4_EVAKEXRQyxEbYjBBXD_5Ne6f7SpBqE
THE PROBLEM: User.Identity.IsAuthenticated check returns False at this stage (in fact the User field is basically empty)
I would have thought, given that the AspNet.ExternalCookie is definitely being sent in the request at step 6 then the user is thereby Authenticated.
So, does anyone know what the middleware would be looking for at this stage in order for it to decode/decrypt/de-serialize the cookie and saturate the User???
Here is the Startup.Auth I have:
public void ConfigureAuth(IAppBuilder app)
{
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
//AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
AuthenticationType = Constants.AuthenticationTypes.MchAdminApplicationCookie,
SlidingExpiration = true,
ExpireTimeSpan = new TimeSpan(10, 0, 0)
});
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
//AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
AuthenticationType = Constants.AuthenticationTypes.MchApiApplicationCookie,
SlidingExpiration = true,
ExpireTimeSpan = new TimeSpan(10, 0, 0)
});
var facebook = new FacebookAuthenticationOptions
{
AppId = "mycode",
AppSecret = "mysecret",
AuthenticationType = "Facebook",
SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie,
Provider = new FacebookAuthenticationProvider
{
OnAuthenticated = async ctx =>
{
if (ctx.User["birthday"] != null)
{
ctx.Identity.AddClaim(new Claim(ClaimTypes.DateOfBirth, ctx.User["birthday"].ToString()));
}
}
}
};
facebook.Scope.Add("user_birthday");
app.UseFacebookAuthentication(facebook);
}
I encountered the same problem. You should add two attributes to ExternalLogin action:
[OverrideAuthentication]
[HostAuthentication(DefaultAuthenticationTypes.ExternalCookie)]