Getting 400 from samltest.id when attempting SP-initiated worflow - saml-2.0

I have used itfoxtec's SAML2 library to implement an SP in my ASP.NET MVC app. I am testing using samltest.id as the IdP. The IdP-initiated workflow works perfectly, but the SP-initiated workflow always gets a 400 error back from samltest.id. I have attempted to look through samltest.id's log to see if an error is being recorded there for my request, but I cannot seem to find anything there.
This is the Action that handles the URL that he user would go to when initiating SSO:
public ActionResult SSOLogin() {
LogManager logger = new LogManager("SSOLogin");
string hostname = this.GetHostname();
SchoolSettings settings = this.GetClientSettings();
if (settings.UseSAMLSSO) {
Saml2Configuration samlConfig = null;
try {
samlConfig = SamlConfigLoader.GetSaml2Config(HttpContext, settings, this.IsSandbox());
} catch (Exception e) {
logger.exception($"loading Saml2Configuration for {hostname}", e);
}
if (samlConfig != null) {
try {
var binding = new Saml2RedirectBinding();
binding.SetRelayStateQuery(new Dictionary<string, string> { { "Home/Index", Url.Content("~/") } });
return binding.Bind(new Saml2AuthnRequest(samlConfig) {
}).ToActionResult();
} catch (Exception e) {
logger.error($"Exception redirecting to IdP. {e.GetType().ToString()}: {e.Message}\n{e.StackTrace}");
ViewBag.ssoerror = $"Error redirecting to IdP for {hostname}";
}
} else {
logger.critical($"Could not load SAML2 configuration for {hostname}");
ViewBag.ssoerror = $"Could not load SAML2 configuration for {hostname}";
}
} else {
ViewBag.ssoerror = "SSO is not configured for this client. Please contact Support";
}
return Redirect("/Home/SSOError");
}
The method that loads a client-specific metadata looks like this:
public static Saml2Configuration GetSaml2Config(HttpContextBase context, SchoolSettings forSchool, bool forSandbox) {
LogManager log = new LogManager("getSaml2Config");
Saml2Configuration config = new Saml2Configuration();
if (!forSandbox) {
config.Issuer = _saml2Issuer;
} else {
config.Issuer = _saml2IssuerSandbox;
}
config.SignatureAlgorithm = _saml2SignatureAlgo;
config.CertificateValidationMode = X509CertificateValidationMode.None;
config.RevocationMode = (X509RevocationMode)Enum.Parse(typeof(X509RevocationMode), ConfigurationManager.AppSettings["Saml2:RevocationMode"]);
config.AllowedAudienceUris.Add(config.Issuer);
var entityDescriptor = new EntityDescriptor();
if (forSchool.SAMLMetadataLocationIsUrl) {
try {
entityDescriptor.ReadIdPSsoDescriptorFromUrl(new Uri(forSchool.SAMLMetadataLocation));
} catch (Exception e) {
log.error($"Exception caught loading metadata from school {forSchool.Hostname} at URL {forSchool.SAMLMetadataLocation}\n Exception {e.GetType().ToString()}: {e.Message}\n{e.StackTrace}");
entityDescriptor.IdPSsoDescriptor = null;
}
} else {
var schoolMetadataPath = context.Server.MapPath("~/App_Data/SAMLMetadata/" + forSchool.SAMLMetadataLocation);
log.info($"Loading metadata for school {forSchool.Hostname} from file {schoolMetadataPath}");
try {
entityDescriptor.ReadIdPSsoDescriptorFromFile(schoolMetadataPath);
} catch (IOException ioe) {
log.error($"IOException caught loading metadata for school {forSchool.Hostname} from file {schoolMetadataPath}: {ioe.Message}\n{ioe.StackTrace}");
entityDescriptor.IdPSsoDescriptor = null;
} catch (Exception e) {
log.error($"Exception caught loading metadata for school {forSchool.Hostname} from file {schoolMetadataPath}\n Exception {e.GetType().ToString()}: {e.Message}\n{e.StackTrace}");
entityDescriptor.IdPSsoDescriptor = null;
}
}
if (entityDescriptor.IdPSsoDescriptor != null) {
if (entityDescriptor.IdPSsoDescriptor.SingleSignOnServices.Count() > 0) {
config.SingleSignOnDestination = entityDescriptor.IdPSsoDescriptor.SingleSignOnServices.First().Location;
} else {
log.error($"WARNING: metadata for {forSchool.Hostname} does not have any SingleSignOnServices that could be parsed.");
}
if (entityDescriptor.IdPSsoDescriptor.SingleLogoutServices.Count() > 0) {
config.SingleLogoutDestination = entityDescriptor.IdPSsoDescriptor.SingleLogoutServices.First().Location;
} else {
log.error($"WARNING: metadata for {forSchool.Hostname} does not have any SingleLogoutServices that could be parsed.");
}
if (entityDescriptor.IdPSsoDescriptor.SigningCertificates.Count() > 0) {
config.SignatureValidationCertificates.AddRange(entityDescriptor.IdPSsoDescriptor.SigningCertificates);
} else {
log.error($"WARNING: metadata for {forSchool.Hostname} does not have any SigningCertificates that could be parsed.");
}
} else {
throw new Exception("IdPSsoDescriptor not loaded from metadata.");
}
return config;
}
If it would help to clarify the situation, I can add the code for the AssertionConsumerService Action which works perfectly in an IdP-initiated scenario.

I discovered the problem. It comes down to this line of code in the GetSaml2Config method:
config.SingleSignOnDestination = entityDescriptor.IdPSsoDescriptor.SingleSignOnServices.First().Location;
This naively takes the first SingleSignOnService element in the metadata and decides that it is the correct one to use, but that was not always the case that this assumption was true. What I really wanted was to get a SingleSignOnService element for and HTTP-POST binding:
config.SingleSignOnDestination = entityDescriptor.IdPSsoDescriptor.SingleSignOnServices.Where(s => s.Binding.ToString().IndexOf("HTTP-POST") > 0).FirstOrDefault()?.Location;
This works well for all of the cases that I have found since.

Your code looks correct.
It is probably an integration issue but very hard to find if the IdP do not log an error message.
What error status message do you get back instead of success, maybe that tells you something.
Maybe the IdP do not accept the SAML 2.0 Authn Response, here is something to look for:
The config.SingleSignOnDestination probably is required
Meybe the IdP requere the request to be signed
It is also possible to add other attributes in the request, do the IdP documentation describe any requirements?

Related

How can I access sqlite database on a webserver in codename one

Pls How can I access sqlite database on the webserver in codename one? I can only use database API to access database on the device. In order to access this on the webserver I think is quite different thing. Pls I need a snippet code on this. Thanks
Use the code below, not tested and you may have to adjust it to suite your need. Leave a comment if there's an issue:
ConnectionRequest req = new ConnectionRequest() {
#Override
protected void handleException(Exception ex) {
//handle error
}
};
req.setUrl(YourURL);
req.setPost(true);
req.setHttpMethod("POST"); //Change to GET if necessary
req.setDuplicateSupported(true);
req.addArgument("argumentToSendThroughPostOrGet1", "value1");
req.addArgument("argumentToSendThroughPostOrGet2", "value2");
NetworkManager.getInstance().addToQueueAndWait(req);
if (req.getResponseCode() == 200) {
Map<String, Object> out = new HashMap<>();
Display.getInstance().invokeAndBlock(() -> {
JSONParser p = new JSONParser();
try (InputStreamReader r = new InputStreamReader(new ByteArrayInputStream(req.getResponseData()))) {
out.putAll(p.parseJSON(r));
} catch (IOException ex) {
//handle error
}
});
if (!out.isEmpty()) {
List<Map<String, Object>> responses = (List<Map<String, Object>>) out.get("response");
for (Object response : responses) {
Map res = (Map) response;
System.out.println(res.get("key"));
}
} else {
//handle error
}
} else {
//handle error
}
TEST JSON RESPONSE:
{
"response": [
{
"key": "I was returned",
}
]
}
EDIT:
To pass data from TextField:
req.addArgument("argumentToSendThroughPostOrGet1", myTextField.getText());
Based on your comment, you can read those arguments in PHP as simple as below:
$var1 = $_POST["argumentToSendThroughPostOrGet1"];
$var1 = $_GET["argumentToSendThroughPostOrGet1"]; // if GET method is used in Codename One
//Or use $_REQUEST which supports both methods but not advisable to be used for production
...
And you can use those variables in your php code normally.
Example of Usage with MySql Query:
class Connection {
function connect() {
$mysqli = mysqli_init();
$mysqli->real_connect("localhost", "username", "password", "databaseName") or die('Could not connect to database!');
$mysqli->query("SET NAMES 'UTF8'");
return $mysqli;
}
function close() {
mysqli_close($this->connect);
}
}
$connection = new Connection();
$mysqli = $connection->connect();
$mysqli->query("SELECT * FROM MyTable WHERE ColumnName LIKE '%$var1%' ORDER BY PrimaryKeyId ASC LIMIT 100");

Set field Accessibility to Custom Salesforce Lead field from Java code

I am working around with Salesforce and force.com API and metadata API, version 36.
I can create a custom field in a Lead object but by default I can see it's hidden and this means I cannot create a new Lead with these custom fields because it returns a bad request (400 status code).
Is there any way by Code to set the custom field Visible?
public boolean createCustomExtTextField(String name, LoginResult metadataLoginResult, int length) {
boolean success = false;
CustomField cs = new CustomField();
cs.setFullName("Lead."+name+"__c");
cs.setLabel("Custom"+name+"Field");
cs.setType(FieldType.LongTextArea);
cs.setLength(length);
cs.setVisibleLines(50); // max 50
try {
MetadataConnection metadataConnection = createMetadataConnection(metadataLoginResult);
SaveResult[] results = metadataConnection.createMetadata(new Metadata[] { cs });
for (SaveResult r : results) {
if (r.isSuccess()) {
success = true;
} else {
System.out.println("Errors were encountered while creating " + r.getFullName());
for (com.sforce.soap.metadata.Error e : r.getErrors()) {
System.out.println("Error message: " + e.getMessage());
System.out.println("Status code: " + e.getStatusCode());
}
}
}
} catch (ConnectionException e) {
e.printStackTrace();
}
return success;
}
I am googling a lot and don't find something that actually helped. So, any hints are welcomed. Thank you.
Finally found a solution to this. I final one for me was to make all custom fields REQUIRED.
CustomField cs = new CustomField();
cs.setFullName("Lead.YourCompanyName" + name + "__c");
cs.setLabel("YourCompanyName" + name);
cs.setRequired(true);
...
com.sforce.soap.enterprise.LoginResult metadataLoginResult = operations.loginToMetadata(username, password, "https://login.salesforce.com/services/Soap/c/36.0");
...
private boolean createFieldInMetadata(LoginResult metadataLoginResult, CustomField cs) {
boolean success = false;
try {
MetadataConnection metadataConnection = createMetadataConnection(metadataLoginResult);
SaveResult[] results = metadataConnection.createMetadata(new Metadata[] { cs });
for (SaveResult r : results) {
if (r.isSuccess()) {
success = true;
} else {
System.out.println("Errors were encountered while creating " + r.getFullName());
for (com.sforce.soap.metadata.Error e : r.getErrors()) {
System.out.println("Error message: " + e.getMessage());
System.out.println("Status code: " + e.getStatusCode());
}
}
}
} catch (Exception e) {
}
return success;
}
And so it will appear in the page layout. Very important to know, a required field cannot have just an empty value set, it must be something. So if not all custom fields are required in your logic and you wanna avoid the entire process of unzipping page layout and zipping it back (however it may be done) just add "N/A" or any char at choice to the required by code but not your project custom fields.
I managed to make the custom Field Level Security visible for "Admin" profile but not Field Accessability to visible. The latter is untested.

Apache shiro Active Directory login via domain name

We are currently using apache shiro to authenticate users against a active directory.
Currently we have the users login via the ldap account name, like firstname.lastname#adsdomain.local
Now we should change the system, that the users can login via the sAMAccountName attribute.
The idea is that the users enter the sAMAccountName name in the login box,
and shiro then does match this against the firstname.lastname#adsdomain.local for the login.
The shiro.ini currently looks like this:
activeDirectoryRealm.systemUsername = systemuser
activeDirectoryRealm.systemPassword = *******
activeDirectoryRealm.searchBase = dc=corp,dc=adsdomain,dc=local
activeDirectoryRealm.url = ldap://<adsserver-ip>:389
activeDirectoryRealm.principalSuffix = #adsdomain.local
With the help of this post in the shiro mailing list I was able to implement a working solution.
The basic steps are:
1. Implement your own queryForAuthenticationInfo method in a inherited class of the ActiveDirectoryRealm
2. Specify to use that new class for the query/login operation
public class AarstockADSRealm extends ActiveDirectoryRealm
{
final private Logger _log = LoggerFactory.getLogger(AarstockADSRealm.class);
public AarstockADSRealm()
{
}
#Override
protected AuthenticationInfo queryForAuthenticationInfo(AuthenticationToken token, LdapContextFactory ldapContextFactory) throws NamingException
{
//final AuthenticationInfo queryForAuthenticationInfo = super.queryForAuthenticationInfo(token, ldapContextFactory);
final UsernamePasswordToken upToken = (UsernamePasswordToken) token;
LdapContext ctx = null;
try
{
ctx = ldapContextFactory.getSystemLdapContext(); // .getLdapContext(upToken.getUsername(), upToken.getPassword());
final String attribName = "userPrincipalName";
final SearchControls searchCtls = new SearchControls(SearchControls.SUBTREE_SCOPE, 1, 0, new String[]
{
attribName
}, false, false);
final NamingEnumeration<SearchResult> search = ctx.search(searchBase, "sAMAccountName={0}", new Object[]
{
upToken.getPrincipal()
}, searchCtls);
if (search.hasMore())
{
final SearchResult next = search.next();
// upToken.setUsername(next.getAttributes().get(attribName).get().toString());
String loginUser= next.getAttributes().get(attribName).get().toString();
_log.info("Loginuser: "+loginUser);
if (search.hasMore())
{
_log.error("More than one user matching: "+upToken.getPrincipal());
throw new RuntimeException("More than one user matching: "+upToken.getPrincipal());
}
else
{
try
{
LdapContext ctx2 = ldapContextFactory.getLdapContext(loginUser, upToken.getPassword());
}
catch (Exception ex)
{
_log.warn("Error in authentication for user "+loginUser, ex);
// We have to rethrow the exception, to indicate invalid login
throw ex;
}
}
}
else
{
_log.info("No user matching: "+upToken.getPrincipal());
throw new RuntimeException("No user matching: "+upToken.getPrincipal());
}
}
catch (NamingException ne)
{
_log.error("Error in ldap name resolving", ne);
// We have to rethrow the exception, to indicate invalid login
throw ne;
} finally
{
LdapUtils.closeContext(ctx);
}
return buildAuthenticationInfo(upToken.getUsername(), upToken.getPassword());
}
}

Get Unread emails from Google API

I'm trying to get the count of unread email using google API, but not able. ANy help is highly appreciated. I'm not getting any error, but the count doesnt match the actual number shown in gmail.
try
{
String serviceAccountEmail = "xxx#developer.gserviceaccount.com";
var certificate = new X509Certificate2(#"C:\Projects\xxx\xyz\API Project-xxxxx.p12", "notasecret", X509KeyStorageFlags.Exportable);
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
User = "xxx#gmail.com",
Scopes = new[] { Google.Apis.Gmail.v1.GmailService.Scope.GmailReadonly }
}.FromCertificate(certificate));
var gmailservice = new Google.Apis.Gmail.v1.GmailService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "GoogleApi3",
});
try
{
List<Message> lst = ListMessages(gmailservice, "xxx#gmail.com", "IN:INBOX IS:UNREAD");
}
catch (Exception e)
{
Console.WriteLine("An error occurred: " + e.Message);
}
}
catch (Exception ex)
{
}
Just do: labels.get(id="INBOX") and it has those types of stats (how many messages in that label, how many are unread, and same for threads).
https://developers.google.com/gmail/api/v1/reference/users/labels/get
You can use the ListMessages method from the API example (included for completeness) for searching:
private static List<Message> ListMessages(GmailService service, String userId, String query)
{
List<Message> result = new List<Message>();
UsersResource.MessagesResource.ListRequest request = service.Users.Messages.List(userId);
request.Q = query;
do
{
try
{
ListMessagesResponse response = request.Execute();
result.AddRange(response.Messages);
request.PageToken = response.NextPageToken;
}
catch (Exception e)
{
Console.WriteLine("An error occurred: " + e.Message);
}
} while (!String.IsNullOrEmpty(request.PageToken));
return result;
}
You can use this search method to find unread messages, for example like this:
List<Message> unreadMessageIDs = ListMessages(service, "me", "is:unread");
The q parameter (query) can be all kinds of stuff (it is the same as the gmail search bar on the top of the web interface), as documented here: https://support.google.com/mail/answer/7190?hl=en.
Note that you only a few parameters of the Message objects are set. If you want to retreive the messages you'll have to use GetMessage method from the api:
public static Message GetMessage(GmailService service, String userId, String messageId)
{
try
{
return service.Users.Messages.Get(userId, messageId).Execute();
}
catch (Exception e)
{
Console.WriteLine("An error occurred: " + e.Message);
}
return null;
}
I agree that the API is not straight forward and misses a lot of functionality.
Solution for .Net:
// Get UNREAD messages
public void getUnreadEmails(GmailService service)
{
UsersResource.MessagesResource.ListRequest Req_messages = service.Users.Messages.List("me");
// Filter by labels
Req_messages.LabelIds = new List<String>() { "INBOX", "UNREAD" };
// Get message list
IList<Message> messages = Req_messages.Execute().Messages;
if ((messages != null) && (messages.Count > 0))
{
foreach (Message List_msg in messages)
{
// Get message content
UsersResource.MessagesResource.GetRequest MsgReq = service.Users.Messages.Get("me", List_msg.Id);
Message msg = MsgReq.Execute();
Console.WriteLine(msg.Snippet);
Console.WriteLine("----------------------");
}
}
Console.Read();
}

OptionSetValue does not contain constructor which takes one argument

I am creating a silverlight application as a web resource for CRM 2011. Now i am creating a ServiceAppointment record in DB and after creating it i want to change its Status to "reserved" instead of requested.
I googled about this and come across the examples like Close a Service Activity Through Code and Microsoft.Crm.Sdk.Messages.SetStateRequest
They all suggesting to use "SetStateRequest" and for using this i have to set the OptionSetValue like
request["State"] = new OptionSetValue(4);
But above line gives me error saying "OptionSetValue does not contain constructor which takes one argument"
BTW i am using SOAP end point of CRM 2011 service in silverlight application
Any ideas friends?
EDIT
Following is my code
var request = new OrganizationRequest { RequestName = "SetStateRequest" };
request["State"] = 3;
request["Status"] = 4;
request["EntityMoniker"] = new EntityReference() { Id = createdActivityId, LogicalName = "serviceappointment" };
crmService.BeginExecute(request,ChangeActivityStatusCallback,crmService);
And my callback function is
private void ChangeActivityStatusCallback(IAsyncResult result) {
OrganizationResponse response;
try
{
response = ((IOrganizationService)result.AsyncState).EndExecute(result);
}
catch (Exception ex)
{
_syncContext.Send(ShowError, ex);
return;
}
}
You must some how be referencing some other OptionSetValue class that is not the Microsoft.Xrm.Sdk one. Try appending the namespace to see if that resolves your issue:
request["State"] = new Microsoft.Xrm.Sdk.OptionSetValue(4);
Also, why are you using late bound on the SetStateRequest? Just use the SetStateRequest class:
public static Microsoft.Crm.Sdk.Messages.SetStateResponse SetState(this IOrganizationService service,
Entity entity, int state, int? status)
{
var setStateReq = new Microsoft.Crm.Sdk.Messages.SetStateRequest();
setStateReq.EntityMoniker = entity.ToEntityReference();
setStateReq.State = new OptionSetValue(state);
setStateReq.Status = new OptionSetValue(status ?? -1);
return (Microsoft.Crm.Sdk.Messages.SetStateResponse)service.Execute(setStateReq);
}
Thanks Daryl for you time and effort. I have solved my problem with the way u have suggested.
I am posting my code that worked for me.
var request = new OrganizationRequest { RequestName = "SetState" };
request["State"] = new OptionSetValue { Value = 3 };
request["Status"] = new OptionSetValue { Value = 4 };
request["EntityMoniker"] = new EntityReference() { Id = createdActivityId, LogicalName = "serviceappointment" };
crmService.BeginExecute(request,ChangeActivityStatusCallback,crmService);
private void ChangeActivityStatusCallback(IAsyncResult result) {
OrganizationResponse response;
try
{
response = ((IOrganizationService)result.AsyncState).EndExecute(result);
}
catch (Exception ex)
{
_syncContext.Send(ShowError, ex);
return;
}
}

Resources