We are implementing MFA using Microsoft Azure. We run wso2 identity server 5.10.4.
What I would like to see happen, is when a user logs in, if they have certain memberOf AD roles, Federation happens. If not, it just uses basic auth/normal log in.
I've been approaching this problem using Adaptive Authentication custom scripting. On a service provider, I created two login steps. Step 1 is basicauth. Step 2 is our MS federated authenticator.
var arrayOfRoles = ["employee","student"];
var onLoginRequest = function(context) {
executeStep(1, {
onSuccess: function (context) {
// Extracting authenticated subject from the first step
var memberOfClaim = 'http://wso2.org/claims/employeeType';
var user = context.currentKnownSubject;
var roles = context.currentKnownSubject.localClaims[memberOfClaim];
foundRole = '-1';
var arrayOfRolesLen = arrayOfRoles.length;
for (var i = 0; i < arrayOfRolesLen; i++) {
searchRole = roles.indexOf(arrayOfRoles[i]);
if (searchRole >= 0) {
foundRole = searchRole;
}
}
if (foundRole >= 0 ) {
Log.info(user.username + ' found a role, indexof=' + foundRole);
// Step 2 is MFA
executeStep(2);
}
}
});
};
The script correctly finds the person's AD memberOf values, and I'm able to execute step 2 using this script. The problem is that a person logs in once to wso2, then if the roles are matched and exectuteStep(2) is invoked, they are prompted with another log in screen for wso2.
How can I prevent a second log in when a person matches the conditions for step 2? Or is this the wrong approach to making a role based decision about when to authenticate using basicauth and when to authenticate using federation?
edit:
Responding to some comments below about using identity-first.
If I setup three steps 1) id first, 2) basic 3) federated, I am prompted for the username again. Step one, a username prompt from wso2. Step two, a username and password prompt. Ditto with step 3.
One difference I see in the below screenshots, is that there is a 'username and password' authenticator. I don't have that available to me in the drop down boxes. Just jwt-basic, and basic. My script using three auth steps looks like this:
var arrayOfRoles = ["PCC_EMPLOYEE_ACTIVE","PCC_STUDENT_CREDIT"];
var onLoginRequest = function(context) {
executeStep(1, {
onSuccess: function (context) {
// Extracting authenticated subject from the first step
var memberOfClaim = 'http://wso2.org/claims/employeeType';
var user = context.currentKnownSubject;
var roles = context.currentKnownSubject.localClaims[memberOfClaim];
foundRole = '-1';
var arrayOfRolesLen = arrayOfRoles.length;
for (var i = 0; i < arrayOfRolesLen; i++) {
searchRole = roles.indexOf(arrayOfRoles[i]);
if (searchRole >= 0) {
foundRole = searchRole;
}
}
Log.info('found role is equal to: ' + foundRole);
if (foundRole >= 0 ) {
Log.info(user.username + ' found a role, indexof=' + foundRole);
// Step 3 is Azure idp.
executeStep(3);
} else {
// Step 2 is basic auth.
executeStep(2);
}
}
});
};```
You may try "identifier First" login with hasRole() to implement your flow.
i.e.
Step1 = Identifier
Step2 = Basic
Step3 = Azure
Let me explain what you have written here using the script.
So you have two auth steps.
Basic auth (username and password)
Federated IDP (Azure)
If you have not enabled adaptive script, after the basic auth step (step1), the user needs to authenticate against the FIDP (step2).
According to the current script that you have written, all the users will have a username and password authentication step (step1). Upon the success of that step, FIDP authentication will be enabled (step2) if the user has a certain set of roles.
Now consider a user who has those specified roles. When that user logins, first the user will see the username password login page. After that, the user will see the azure login page if the user has no active session in Azure (If SSO is enabled, the user might not see the second screen). So it is expected to see two login screens.
So what you have done here is correct, but the user experience is kind of weird. A similar scenario is explained here.
Edited: The better approach is to use the identifier first login method. You can read it here. Please find the approach that I have tried.
I have added 3 steps as follows.
Next I have added a role based adaptive script.
With this approach if I have the role admin, I will only see a input box to provide my password.
But for google it will show google sign-in. If you already have a session then only the consent will be seen. We cannot skip this since this is not in our control.
Related
I want Use this link:
https://docs.wso2.com/display/IS570/Adaptive+Authentication
I want Role1 Can't Login to my Application and Write this Code:
and it's Don't Work and Role1 can login to my application.
How can Write this script?
// Role-Based from Template...
// This script will step up authentication for any user belonging
// to one of the given roles
// If the user has any of the below roles, authentication will be stepped up
var rolesToStepUp = ['Role1'];
function onLoginRequest(context) {
executeStep(1, {
onSuccess: function (context) {
// Extracting authenticated subject from the first step
var user = context.currentKnownSubject;
// Checking if the user is assigned to one of the given roles
var hasRole = hasAnyOfTheRoles(user, rolesToStepUp);
if (hasRole) {
Log.info(user.username + ' Has one of Roles: ' + rolesToStepUp.toString());
return false;
}
}
});
}
// End of Role-Based.......
If you didn't get the log Log.info(user.username + ' Has one of Roles: ' + rolesToStepUp.toString()); check your condition. If your Role is an internal role, change rolesToStepUp variable as follows and try out.
var rolesToStepUp = ['Internal/Role1'];
Have a look on https://stackoverflow.com/a/66013019/10055162 for more details
In order to fail the user login, just returning false doesn't work. Use sendError utility function (https://docs.wso2.com/display/IS570/Adaptive+Authentication+JS+API+Reference#AdaptiveAuthenticationJSAPIReference-sendError(url,parameters)).
Check user-age-based script for sendError usage (https://docs.wso2.com/display/IS570/Configuring+User-Age-Based+Adaptive+Authentication)
We use WSO2 5.10.1 for SSO and I am trying to get adaptive MFA working but having some trouble. The IP-based adaptive authentication template works, but for some reason, the role-based template does not. For some reason, the variable hasRole always returns false, even though my test user is a member of the internal role called staff2. staff2 is an internal role containing only the test user.
The wso2carbon.log with the info is below. I also included the script I am using, which is the default role-based template PLUS a few modifications I made to get a little more information in the log.
Log:
TID: [-1234] [] [2021-02-02 10:14:34,684] [badcbf3d-288c-4d3a-8abc-372763d87e0b] INFO {org.wso2.carbon.identity.application.authentication.framework.config.model.graph.js.JsLogger} - Working so far
TID: [-1234] [] [2021-02-02 10:14:34,684] [badcbf3d-288c-4d3a-8abc-372763d87e0b] INFO {org.wso2.carbon.identity.application.authentication.framework.config.model.graph.js.JsLogger} - false string info
TID: [-1234] [] [2021-02-02 10:14:34,684] [badcbf3d-288c-4d3a-8abc-372763d87e0b] INFO {org.wso2.carbon.identity.application.authentication.framework.config.model.graph.js.JsLogger} - <testuser> Has one of Roles: [staff2]
My Code:
// Role-Based from Template...
// This script will step up authentication for any user belonging
// to one of the given roles
// If the user has any of the below roles, authentication will be stepped up
var rolesToStepUp = ['staff2'];
var onLoginRequest = function(context) {
executeStep(1, {
onSuccess: function (context) {
// Extracting authenticated subject from the first step
var user = context.currentKnownSubject;
// Checking if the user is assigned to one of the given roles
var hasRole = hasAnyOfTheRoles(user, rolesToStepUp);
Log.info('Working so far');
Log.info(hasRole + ' string info');
if (5 > 1) {
Log.info(user.username + ' Has one of Roles: ' + rolesToStepUp.toString());
executeStep(2);
}
}
});
};
// End of Role-Based.......
Change your rolesToStepUp variable as follows and try out.
var rolesToStepUp = ['Internal/staff2'];
If you use the variable as var rolesToStepUp = ['staff2'];, it checks whether the user has a role named 'staff2' in the PRIMARY userstore. For example, if you create a Role by selection domain- PRIMARY and role name- staff2, the user who gets assigned to that role will be able to go through 2 steps according to your script.
If you want to use a role other than a role attached to PRIMARY userstore, you need to define the domain in the var rolesToStepUp array.
Since you have created an internal role, var rolesToStepUp should be defined as var rolesToStepUp = ['Internal/staff2'];
I want to use a phone number as the username for my app and i want to be able to make it simple to sign up by just having to verify the phone number each time they want to login - no messy password remembering business.
How to do this with AWS Cognito User Pool as its asking me to mandatorily configure a password for each user.
I thought of using a dummy password for each user and configure mandatory user verification. Everytime the user sign out i can "Unverify" the user so that next time they would automatically be asked to verify the phone number. Also i would wire up my app to only "login" if the user is verified.
Please let me know if the is the best approach :( I'm new to AWS and i could't find any posts for this scenario.
Thanks !!
Since AWS Cognito is not currently supporting passwordless authentication you need to implement a workaround with random password stored externally. You can implement the authentication flow as follows.
After user Signup (Also ask for mobile number and make it mandatory), store the Mobile number, Username and Password also in Dynamodb encrypted with AWS KMS (For additional security).
You can use MFA with mobile number for authentication challenge so that after the user enters mobile number and press login(In frontend), in the backend you can automatically do username password matching(Passthrough) and trigger the MFA to send a code for user's mobile and verify that using the AWS Cognito SDK (Without implementing custom mobile message and challenge).
If you plan to implement the flow manually(Without MFA) to send the SMS & validation, you can use AWS SNS for the purpose.
Check the following code sample to understand the insight of MFA and refer this link for more details.
var userData = {
Username : 'username',
Pool : userPool
};
cognitoUser = new AWSCognito.CognitoIdentityServiceProvider.CognitoUser(userData);
var authenticationData = {
Username : 'username',
Password : 'password',
};
var authenticationDetails = new AWSCognito.CognitoIdentityServiceProvider.AuthenticationDetails(authenticationData);
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: function (result) {
alert('authentication successful!')
},
onFailure: function(err) {
alert(err);
},
mfaRequired: function(codeDeliveryDetails) {
var verificationCode = prompt('Please input verification code' ,'');
cognitoUser.sendMFACode(verificationCode, this);
}
});
Note: Here the MFA with mobile number is not used for the purpose of MFA but as a workaround to meet your requirement.
This is a slightly different spin to what the OP is requesting as it uses a single secret, but I think it may help others who land on this question.
I was able to do this by creating custom lambdas for the Cognito triggers: Define Auth Challenge, Create Auth Challenge & Verify Auth Challenge.
My requirement was that I wanted my backend to use a secret to then get access & refresh tokens for any Cognito user.
Define Auth Challenge Lambda
exports.handler = async event => {
if (
event.request.session &&
event.request.session.length >= 3 &&
event.request.session.slice(-1)[0].challengeResult === false
) {
// The user provided a wrong answer 3 times; fail auth
event.response.issueTokens = false;
event.response.failAuthentication = true;
} else if (
event.request.session &&
event.request.session.length &&
event.request.session.slice(-1)[0].challengeResult === true
) {
// The user provided the right answer; succeed auth
event.response.issueTokens = true;
event.response.failAuthentication = false;
} else {
// The user did not provide a correct answer yet; present challenge
event.response.issueTokens = false;
event.response.failAuthentication = false;
event.response.challengeName = 'CUSTOM_CHALLENGE';
}
return event;
};
Create Auth Challenge Lambda
exports.handler = async event => {
if (event.request.challengeName == 'CUSTOM_CHALLENGE') {
// The value set for publicChallengeParameters is arbitrary for our
// purposes, but something must be set
event.response.publicChallengeParameters = { foo: 'bar' };
}
return event;
};
Verify Auth Challenge Lambda
exports.handler = async event => {
if (event.request.challengeName == 'CUSTOM_CHALLENGE') {
// The value set for publicChallengeParameters is arbitrary for our
// purposes, but something must be set
event.response.publicChallengeParameters = { foo: 'bar' };
}
return event;
};
I was then able to use some JS, using amazon-cognito-identity-js, to provide the secret and get the tokens:
var authenticationData = {
Username : 'username'
};
var authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(authenticationData);
var poolData = {
UserPoolId : '...', // Your user pool id here
ClientId : '...' // Your client id here
};
var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
var userData = {
Username : 'username',
Pool : userPool
};
var cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
cognitoUser.setAuthenticationFlowType('CUSTOM_AUTH');
cognitoUser.initiateAuth(authenticationDetails, {
onSuccess: function(result) {
// User authentication was successful
},
onFailure: function(err) {
// User authentication was not successful
},
customChallenge: function(challengeParameters) {
// User authentication depends on challenge response
var challengeResponses = 'secret'
cognitoUser.sendCustomChallengeAnswer(challengeResponses, this);
}
});
this may work, but storing password in dynamoDB may have issues, considering security. instead we can try like this:
option#1: - user sign ups with username and password.
setup cognito triggers - we can use lambda functions.
A. Create Auth Challenge
B. Define Auth Challenge
C. Verify Auth Challenge Response
client app should implement CUSTOM_CHALLENGE authentication flow.
ask user to enter registered phone number, pass this in username field. trigger B will understand the request and passes flow to trigger A, Trigger A will generate random code 5. use AWS SNS service to send SMS to user mobile number
Trigger C will validate OTP and allows login
points to consider:
a. setup phone number as alias (select Also allow sign in with verified phone number)
b. make phone number field as verifiable (this allows user to receive OTP)
option#1: - user sign ups without username and password.
cognito setup
setup phone number as alias (select Also allow sign in with verified phone number)
make phone number field as verifiable (this allows user to receive OTP)
during signup don't ask user to provide username and password, just ask phone number
generate UUID for both username and password to be unique and pass these to cognito along with phone number
Cognito sends OTP code to user for account confirmation.
for phone number with OTP login setup triggers as explained in above option.
for triggers code,refer
aws cognito pool with multiple sign in options
Identity server is implemented and working well. Google login is working and is returning several claims including email.
Facebook login is working, and my app is live and requests email permissions when a new user logs in.
The problem is that I can't get the email back from the oauth endpoint and I can't seem to find the access_token to manually request user information. All I have is a "code" returned from the facebook login endpoint.
Here's the IdentityServer setup.
var fb = new FacebookAuthenticationOptions
{
AuthenticationType = "Facebook",
SignInAsAuthenticationType = signInAsType,
AppId = ConfigurationManager.AppSettings["Facebook:AppId"],
AppSecret = ConfigurationManager.AppSettings["Facebook:AppSecret"]
};
fb.Scope.Add("email");
app.UseFacebookAuthentication(fb);
Then of course I've customized the AuthenticateLocalAsync method, but the claims I'm receiving only include name. No email claim.
Digging through the source code for identity server, I realized that there are some claims things happening to transform facebook claims, so I extended that class to debug into it and see if it was stripping out any claims, which it's not.
I also watched the http calls with fiddler, and I only see the following (apologies as code formatting doesn't work very good on urls. I tried to format the querystring params one their own lines but it didn't take)
(facebook.com)
/dialog/oauth
?response_type=code
&client_id=xxx
&redirect_uri=https%3A%2F%2Fidentity.[site].com%2Fid%2Fsignin-facebook
&scope=email
&state=xxx
(facebook.com)
/login.php
?skip_api_login=1
&api_key=xxx
&signed_next=1
&next=https%3A%2F%2Fwww.facebook.com%2Fv2.7%2Fdialog%2Foauth%3Fredirect_uri%3Dhttps%253A%252F%252Fidentity.[site].com%252Fid%252Fsignin-facebook%26state%3Dxxx%26scope%3Demail%26response_type%3Dcode%26client_id%3Dxxx%26ret%3Dlogin%26logger_id%3Dxxx&cancel_url=https%3A%2F%2Fidentity.[site].com%2Fid%2Fsignin-facebook%3Ferror%3Daccess_denied%26error_code%3D200%26error_description%3DPermissions%2Berror%26error_reason%3Duser_denied%26state%3Dxxx%23_%3D_
&display=page
&locale=en_US
&logger_id=xxx
(facebook.com)
POST /cookie/consent/?pv=1&dpr=1 HTTP/1.1
(facebook.com)
/login.php
?login_attempt=1
&next=https%3A%2F%2Fwww.facebook.com%2Fv2.7%2Fdialog%2Foauth%3Fredirect_uri%3Dhttps%253A%252F%252Fidentity.[site].com%252Fid%252Fsignin-facebook%26state%3Dxxx%26scope%3Demail%26response_type%3Dcode%26client_id%3Dxxx%26ret%3Dlogin%26logger_id%3Dxxx
&lwv=100
(facebook.com)
/v2.7/dialog/oauth
?redirect_uri=https%3A%2F%2Fidentity.[site].com%2Fid%2Fsignin-facebook
&state=xxx
&scope=email
&response_type=code
&client_id=xxx
&ret=login
&logger_id=xxx
&hash=xxx
(identity server)
/id/signin-facebook
?code=xxx
&state=xxx
I saw the code parameter on that last call and thought that maybe I could use the code there to get the access_token from the facebook API https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow
However when I tried that I get a message from the API telling me the code has already been used.
I also tried to change the UserInformationEndpoint to the FacebookAuthenticationOptions to force it to ask for the email by appending ?fields=email to the end of the default endpoint location, but that causes identity server to spit out the error "There was an error logging into the external provider. The error message is: access_denied".
I might be able to fix this all if I can change the middleware to send the request with response_type=id_token but I can't figure out how to do that or how to extract that access token when it gets returned in the first place to be able to use the Facebook C# sdk.
So I guess any help or direction at all would be awesome. I've spent countless hours researching and trying to solve the problem. All I need to do is get the email address of the logged-in user via IdentityServer3. Doesn't sound so hard and yet I'm stuck.
I finally figured this out. The answer has something to do with Mitra's comments although neither of those answers quite seemed to fit the bill, so I'm putting another one here. First, you need to request the access_token, not code (authorization code) from Facebook's Authentication endpoint. To do that, set it up like this
var fb = new FacebookAuthenticationOptions
{
AuthenticationType = "Facebook",
SignInAsAuthenticationType = signInAsType,
AppId = ConfigurationManager.AppSettings["Facebook:AppId"],
AppSecret = ConfigurationManager.AppSettings["Facebook:AppSecret"],
Provider = new FacebookAuthenticationProvider()
{
OnAuthenticated = (context) =>
{
context.Identity.AddClaim(new System.Security.Claims.Claim("urn:facebook:access_token", context.AccessToken, ClaimValueTypes.String, "Facebook"));
return Task.FromResult(0);
}
}
};
fb.Scope.Add("email");
app.UseFacebookAuthentication(fb);
Then, you need to catch the response once it's logged in. I'm using the following file from the IdentityServer3 Samples Repository, which overrides (read, provides functionality) for the methods necessary to log a user in from external sites. From this response, I'm using the C# Facebook SDK with the newly returned access_token claim in the ExternalAuthenticationContext to request the fields I need and add them to the list of claims. Then I can use that information to create/log in the user.
public override async Task AuthenticateExternalAsync(ExternalAuthenticationContext ctx)
{
var externalUser = ctx.ExternalIdentity;
var claimsList = ctx.ExternalIdentity.Claims.ToList();
if (externalUser.Provider == "Facebook")
{
var extraClaims = GetAdditionalFacebookClaims(externalUser.Claims.First(claim => claim.Type == "urn:facebook:access_token"));
claimsList.Add(new Claim("email", extraClaims.First(k => k.Key == "email").Value.ToString()));
claimsList.Add(new Claim("given_name", extraClaims.First(k => k.Key == "first_name").Value.ToString()));
claimsList.Add(new Claim("family_name", extraClaims.First(k => k.Key == "last_name").Value.ToString()));
}
if (externalUser == null)
{
throw new ArgumentNullException("externalUser");
}
var user = await userManager.FindAsync(new Microsoft.AspNet.Identity.UserLoginInfo(externalUser.Provider, externalUser.ProviderId));
if (user == null)
{
ctx.AuthenticateResult = await ProcessNewExternalAccountAsync(externalUser.Provider, externalUser.ProviderId, claimsList);
}
else
{
ctx.AuthenticateResult = await ProcessExistingExternalAccountAsync(user.Id, externalUser.Provider, externalUser.ProviderId, claimsList);
}
}
And that's it! If you have any suggestions for simplifying this process, please let me know. I was going to modify this code to do perform the call to the API from FacebookAuthenticationOptions, but the Events property no longer exists apparently.
Edit: the GetAdditionalFacebookClaims method is simply a method that creates a new FacebookClient given the access token that was pulled out and queries the Facebook API for the other user claims you need. For example, my method looks like this:
protected static JsonObject GetAdditionalFacebookClaims(Claim accessToken)
{
var fb = new FacebookClient(accessToken.Value);
return fb.Get("me", new {fields = new[] {"email", "first_name", "last_name"}}) as JsonObject;
}
I'm using Parse.com and trying to set up user sign up with Facebook.
Upon authentication with Facebook for the first time a beforeSave is called on _User to fetch additional user details:
function UserBeforeSave(request, response){
var user = request.object,
auth = user.get('authData');
// check if user is newly registered
if (!user.existed()) {
// Check if a user signs up with facebook
if (Parse.FacebookUtils.isLinked(request.object)) {
// Query Graph API for user details
Parse.Cloud.httpRequest({
url:'https://graph.facebook.com/v2.2/me?access_token=' + auth.facebook.access_token,
success:function(httpResponse){
// Map facebook data to user object
if (httpResponse.data.first_name) request.object.set('first_name', httpResponse.data.first_name);
if (httpResponse.data.last_name) request.object.set('last_name', httpResponse.data.last_name);
if (httpResponse.data.email) request.object.set('email', httpResponse.data.email);
response.success();
},
error:function(httpResponse){
console.error(httpResponse);
response.error();
}
});
} else {
response.success();
}
} else {
response.success();
}
}
Problem is that that email line is actually breaking the operation with error:
Can't modify email in the before save trigger
I tried moving this code to the afterSave but the authdata is not available there making it difficult to call the FB API. The email is therefore left blank at the moment.
I'm assuming this is a very common use case of integrating Parse with the Facebook API, am I missing something in the integration process that automatically fetches the email?
I just do the graph query client-side and set email there. This works fine.
Is there a reason you want to do it in the before/afterSave on the User?