I have a serialized SOAP request message with a SAML token holder-of-key that works against a vendor service. I want to create a demonstration program in C# to produce a similar request. To do this, I want to write a client that creates its own SAML token.
I've got a SAML2 token created successfully from a self signed cert and I am able to associate it to the request using the ChannelFactoryOperations.CreateChannelWithIssuedToken approach (.Net 4.0). Everything is working great but I can't figure out the C# required to place the signature after the assertion and use the SAML token as the signature KeyIdentifier to sign the timestamp. I'm not even sure what I am asking, but it seems like the signature after the token itself should be the easy part. But, the only way I've gotten the SAML to come out in the request is by declaring it of type BearerKey. But BearerKey appears to omit the signature after the Assertion. It seems I want SymmetricKey, but the token "has no keys." How do I make a signature element like this appear after the Assertion?
Here URI="#_1" is referring to the WS-Security timestamp (not shown) above.
Hi folks I can't believe I finally figured all of this out. This code loads up a self signed cert, generates a SAML token and then endorses the message with the SAML token. The problem I was having was with the "token has no keys" error. That was solved by creating an issuerToken and a key and passing that in to the token constructor. See below. I think the most helpful information I found online is this great post here http://devproconnections.com/development/generating-saml-tokens-wif-part-2
X509Certificate2 cert = new X509Certificate2("C:\\Users\\foobar\\desktop\\test.pfx", "test", X509KeyStorageFlags.MachineKeySet);
RSACryptoServiceProvider rsa = cert.PrivateKey as RSACryptoServiceProvider;
RsaSecurityKey rsaKey = new RsaSecurityKey(rsa);
RsaKeyIdentifierClause rsaClause = new RsaKeyIdentifierClause(rsa);
SecurityKeyIdentifier signingSki = new SecurityKeyIdentifier(new SecurityKeyIdentifierClause[] { rsaClause });
SigningCredentials signingCredentials = new SigningCredentials(rsaKey, SecurityAlgorithms.RsaSha1Signature, SecurityAlgorithms.Sha1Digest, signingSki);
Saml2NameIdentifier saml2NameIdentifier = new Saml2NameIdentifier("C=US,O=hi mom,CN=test", new System.Uri("urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName"));
Saml2Assertion saml2Assertion2 = new Saml2Assertion(saml2NameIdentifier);
saml2Assertion2.SigningCredentials = signingCredentials;
Saml2Subject saml2Subject = new Saml2Subject();
saml2NameIdentifier = new Saml2NameIdentifier("foo#bar.edu", new System.Uri("urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName"));
saml2Subject.NameId = saml2NameIdentifier;
Saml2SubjectConfirmationData subjectConfirmationData = new Saml2SubjectConfirmationData();
Saml2SubjectConfirmation subjectConfirmation = new Saml2SubjectConfirmation(new Uri("urn:oasis:names:tc:SAML:2.0:cm:holder-of-key"));
subjectConfirmation.SubjectConfirmationData = subjectConfirmationData;
subjectConfirmationData.KeyIdentifiers.Add(signingSki);
saml2Subject.SubjectConfirmations.Add(subjectConfirmation);
saml2Assertion2.Subject = saml2Subject;
Saml2AuthenticationContext saml2AuthCtxt = new Saml2AuthenticationContext(new Uri("urn:oasis:names:tc:SAML:2.0:ac:classes:X509"));
Saml2AuthenticationStatement saml2AuthStatement = new Saml2AuthenticationStatement(saml2AuthCtxt);
saml2AuthStatement.SessionIndex = "123456";
saml2Assertion2.Statements.Add(saml2AuthStatement);
Saml2AttributeStatement saml2AttStatement = new Saml2AttributeStatement();
Saml2Attribute saml2Attribute = new Saml2Attribute("urn:oasis:names:tc:xspa:1.0:subject:subject-id", "foo bar test");
saml2AttStatement.Attributes.Add(saml2Attribute);
saml2Attribute = new Saml2Attribute("urn:oasis:names:tc:xspa:1.0:subject:organization", "urn:oid:"+senderOid);
saml2AttStatement.Attributes.Add(saml2Attribute);
saml2Attribute = new Saml2Attribute("urn:oasis:names:tc:xspa:1.0:subject:organization-id", "urn:oid:" + senderOid);
saml2AttStatement.Attributes.Add(saml2Attribute);
saml2Attribute = new Saml2Attribute("urn:nhin:names:saml:homeCommunityId", "urn:oid:" + senderOid);
saml2AttStatement.Attributes.Add(saml2Attribute);
saml2Attribute = new Saml2Attribute("urn:oasis:names:tc:xacml:2.0:subject:role");
saml2AttStatement.Attributes.Add(saml2Attribute);
saml2Assertion2.Statements.Add(saml2AttStatement);
List<SecurityKey> keyList = new List<SecurityKey>();
keyList.Add(rsaKey);
ReadOnlyCollection<SecurityKey> keys = new ReadOnlyCollection<SecurityKey>(keyList);
X509SecurityToken issuerToken = new X509SecurityToken(cert);
Saml2SecurityToken token2 = new Saml2SecurityToken(saml2Assertion2,keys,issuerToken);
XcpdRespondingGatewaySyncService.RespondingGatewaySyncClient myClient = new XcpdRespondingGatewaySyncService.RespondingGatewaySyncClient("IRespondingGatewaySync2");
CustomBinding customBinding = myClient.Endpoint.Binding as CustomBinding;
SecurityBindingElement element = customBinding.Elements.Find<SecurityBindingElement>();
IssuedSecurityTokenParameters tokenParameters = element.EndpointSupportingTokenParameters.Signed[0].Clone() as IssuedSecurityTokenParameters;
tokenParameters.TokenType = System.IdentityModel.Tokens.SecurityTokenTypes.Saml;
tokenParameters.RequireDerivedKeys = false;
tokenParameters.KeyType = SecurityKeyType.SymmetricKey;
element.EndpointSupportingTokenParameters.Signed.Clear();
element.EndpointSupportingTokenParameters.Endorsing.Add(tokenParameters);
myClient.ChannelFactory.Credentials.SupportInteractive = false;
myClient.ChannelFactory.ConfigureChannelFactory();
XcpdRespondingGatewaySyncService.IRespondingGatewaySync myChannel = ChannelFactoryOperations.CreateChannelWithIssuedToken(myClient.ChannelFactory, token2);
Related
I have a .Net Standard client application running on UWP.
My client application contacts the server that generates a sas key like so:
var myPrivateStorageAccount = CloudStorageAccount.Parse(mystorageAccountKey);
var myPrivateTableClient = myPrivateStorageAccount.CreateCloudTableClient();
SharedAccessTablePolicy pol = new SharedAccessTablePolicy()
{
SharedAccessExpiryTime = DateTime.UtcNow.AddMinutes(59),
Permissions = SharedAccessTablePermissions.Query | SharedAccessTablePermissions.Add
};
CloudTable myPrivateTable = myPrivateTableClient.GetTableReference(tableName);
String sas = myPrivateTable.GetSharedAccessSignature(pol);
return sas;
My client application then runs the following:
StorageCredentials creds = new StorageCredentials(sas);
this.tableClient = new CloudTableClient(tableServiceURI, creds);
this.table = tableClient.GetTableReference(tableName);
TableQuery<DynamicTableEntity> projectionQuery = new TableQuery<DynamicTableEntity>().Select(new string[] { "DocumentName" }).Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, cc));
var res = await table.ExecuteQuerySegmentedAsync<DynamicTableEntity>(projectionQuery, null);
and gets the following error:
Server failed to authenticate the request. Make sure the value of
Authorization header is formed correctly including the signature. sr
is mandatory. Cannot be empty
but as this is tablestorage I dont think sr is required
and my SAS key looks fine to me:
?sv=2018-03-28&tn=MyTable&sig=RandomSig151235341543&st=2019-01-17T12%3A00%3A28Z&se=2019-01-17T12%3A59%3A28Z&sp=ra
so whats the problem here?
Ok so this is kind of stupid but I will post anyway.
I was sending the request to:
https://myaccount.blob.core.windows.net/
and should have been sending the request to:
https://myaccount.table.core.windows.net/
There:
I am building TimeStamp Authorization by BouncyCastle (c#), everything is ok, however, I can't figure out how I can embed OCSP response when sign by timestamp key, by default, it just use CRL which defined in timestamp certificate.
TimeStampTokenGenerator tokenGen = new TimeStampTokenGenerator(theIssuerPrivateKey, cert, TspAlgorithms.Sha512, "1.2");
IList certList = new ArrayList();
certList.Add(cert);
IX509Store x509Certs = X509StoreFactory.Create("Certificate/Collection", new X509CollectionStoreParameters(certList));
tokenGen.SetCertificates(x509Certs);
TimeStampRequestGenerator reqGen = new TimeStampRequestGenerator();
reqGen.SetCertReq(true);
TimeStampRequest req = reqGen.Generate(TspAlgorithms.Sha512, content);
TimeStampResponseGenerator respGen = new TimeStampResponseGenerator(tokenGen, TspAlgorithms.Allowed);
TimeStampResponse resp = respGen.Generate(req, new BigInteger("23"), signTime);
//embedded some ocsp response to tokenGen maybe? But how?
Please help....
I'm writing client using WCF for demo settings provided by PingFederate in the similar way as described in this post (Getting a Token from PingFederate using WIF).
My problem is that on the second step I need to exchange SAML token to lightweight SSO token on SP.
I'm getting exception while serialization of response from the SP
ID3135: The element 'TokenType' with namespace 'http://docs.oasis-open.org/ws-sx/ws-trust/200512' has value 'BASE64BINARY' which is not an absolute URI.
as I understand WS2007FederationHttpBinding can't handle that type of tokens.
I'm looking for explanation how such scenario should be handled.
The the client code is as following (after getting SAML token from IDP)
var binding = new WS2007FederationHttpBinding();
binding.Security.Mode = WSFederationHttpSecurityMode.TransportWithMessageCredential;
binding.Security.Message.IssuedKeyType = SecurityKeyType.BearerKey;
binding.Security.Message.EstablishSecurityContext = false;
var factory = new WSTrustChannelFactory(binding, new EndpointAddress(SP_EndPoint));
factory.TrustVersion = TrustVersion.WSTrust13;
factory.Credentials.SupportInteractive = false;
var rst = new RequestSecurityToken
{
RequestType = RequestTypes.Issue,
//AppliesTo = new EndpointReference(#"http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous"),
};
RequestSecurityTokenResponse rstr;
var channel = factory.CreateChannelWithIssuedToken(idpToken);
SecurityToken spToken = channel.Issue(rst, out rstr);
return spToken;
I am trying to follow the steps outlined here in order to acquire the access token for use with the Google Calendar API with OAuth2. After attempting to put together and sign the jwt, I always end up with a 400 "Bad request" response, with the error "invalid_grant".
I have followed the steps quite closely and have meticulously checked each line multiple times. I have also exhaustively looked through each and every post on the topic that I could find. I am so thoroughly stumped that I have written my first ever SO question after many years of finding solutions online.
Commonly proposed solutions that I have already tried:
1) My system clock is sync'd with ntp time
2) I am using the email for iss, NOT the client ID.
3) My issued times and expiration times are in UTC
4) I did look into the access_type=offline parameter, but it does not seem to apply in this server-to-server scenario.
5) I did not specify the prn parameter.
6) Various other misc things
I am aware that there are Google libraries that help manage this, but I have reasons for why I need to get this working by signing the jwt myself without using the provided libraries. Also, many of the questions and samples I've seen thus far seem to be using accounts.google.com/o/oauth2/auth for the base url, whereas the documentation I linked above seems to specify that the request go to www.googleapis.com/oauth2/v3/token instead (so it seems that many of the existing questions might apply to a different scenario). In any case, I'm totally stumped and don't know what else to try. Here is my C# code, with a few specific strings redacted.
public static string GetBase64UrlEncoded(byte[] input)
{
string value = Convert.ToBase64String(input);
value = value.Replace("=", string.Empty).Replace('+', '-').Replace('/', '_');
return value;
}
static void Main(string[] args)
{
DateTime baseTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
DateTime now = DateTime.Now.ToUniversalTime();
int ticksIat = ((int)now.Subtract(baseTime).TotalSeconds);
int ticksExp = ((int)now.AddMinutes(55).Subtract(baseTime).TotalSeconds);
string jwtHeader = #"{""typ"":""JWT"", ""alg"":""RS256""}";
string jwtClaimSet = string.Format(#"{{""iss"":""************-********************************#developer.gserviceaccount.com""," +
#"""scope"":""https://www.googleapis.com/auth/calendar.readonly""," +
#"""aud"":""https://www.googleapis.com/oauth2/v3/token"",""exp"":{0},""iat"":{1}}}", ticksExp, ticksIat);
byte[] headerBytes = Encoding.UTF8.GetBytes(jwtHeader);
string base64jwtHeader = GetBase64UrlEncoded(headerBytes);
byte[] claimSetBytes = Encoding.UTF8.GetBytes(jwtClaimSet);
string base64jwtClaimSet = GetBase64UrlEncoded(claimSetBytes);
string signingInputString = base64jwtHeader + "." + base64jwtClaimSet;
byte[] signingInputBytes = Encoding.UTF8.GetBytes(signingInputString);
X509Certificate2 pkCert = new X509Certificate2("<path to cert>.p12", "notasecret");
RSACryptoServiceProvider rsa = (RSACryptoServiceProvider)pkCert.PrivateKey;
CspParameters cspParam = new CspParameters
{
KeyContainerName = rsa.CspKeyContainerInfo.KeyContainerName,
KeyNumber = rsa.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2
};
RSACryptoServiceProvider cryptoServiceProvider = new RSACryptoServiceProvider(cspParam) { PersistKeyInCsp = false };
byte[] signatureBytes = cryptoServiceProvider.SignData(signingInputBytes, "SHA256");
string signatureString = GetBase64UrlEncoded(signatureBytes);
string finalJwt = signingInputString + "." + signatureString;
HttpClient client = new HttpClient();
string url = "https://www.googleapis.com/oauth2/v3/token?grant_type=urn%3aietf%3aparams%3aoauth%3agrant-type%3ajwt-bearer&assertion=" + finalJwt;
HttpResponseMessage message = client.PostAsync(url, new StringContent(string.Empty)).Result;
string result = message.Content.ReadAsStringAsync().Result;
}
This is using a google "Service Account" which I set up on my google account, with a generated private key and its corresponding .p12 file used directly.
Has anyone gotten this approach to work? I would strongly appreciate any help at all!
You're POSTing to the token endpoint but the parameters are sent as part of the query string. You should send the parameters as URL form-encoded values in the POST body. For example:
var params = new List<KeyValuePair<string, string>>();
params.Add(new KeyValuePair<string, string>("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"));
params.Add(new KeyValuePair<string, string>("assertion", finalJwt));
var content = new FormUrlEncodedContent(pairs);
var message = client.PostAsync(url, content).Result;
try this to get access token-
String serviceAccountEmail = "xxxxxxx.gserviceaccount.com";
String keyFilePath = System.Web.HttpContext.Current.Server.MapPath("~/Content/Security/file.p12"); ////.p12 file location
if (!File.Exists(keyFilePath))
{
Console.WriteLine("An Error occurred - Key file does not exist");
return null;
}
string[] scopes = new string[] {
CloudVideoIntelligenceService.Scope.CloudPlatform, ///CloudVideoIntelligence scope
YouTubeService.Scope.YoutubeForceSsl, ///Youtube scope
TranslateService.Scope.CloudTranslation ///Translation scope
};
var certificate = new X509Certificate2(keyFilePath, "notasecret", X509KeyStorageFlags.Exportable);
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
Scopes = scopes
}.FromCertificate(certificate));
var token = Google.Apis.Auth.OAuth2.GoogleCredential.FromServiceAccountCredential(credential).UnderlyingCredential.GetAccessTokenForRequestAsync().Result;//retrive token
return token;
Here's my WCF service code:
ServiceHost svh = new ServiceHost(typeof(MyClass));
var tcpbinding = new NetTcpBinding(SecurityMode.TransportWithMessageCredential, true);
//security
tcpbinding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;
svh.Credentials.UserNameAuthentication.CustomUserNamePasswordValidator = new BWUserNamePasswordValidator();
svh.Credentials.UserNameAuthentication.UserNamePasswordValidationMode =UserNamePasswordValidationMode.Custom;
svh.Credentials.ServiceCertificate.Certificate = GenerateCertificate(myCert);
svh.AddServiceEndpoint(typeof(IMyClass), tcpbinding, location);
svh.Open();
And here's the code, which I'm using to generate the certificate:
static X509Certificate2 GenerateCertificate(string certName)
{
var keypairgen = new RsaKeyPairGenerator();
keypairgen.Init(new KeyGenerationParameters(new SecureRandom(new CryptoApiRandomGenerator()), 1024));
var keypair = keypairgen.GenerateKeyPair();
var gen = new X509V3CertificateGenerator();
var CN = new X509Name("CN=" + certName);
var SN = BigInteger.ProbablePrime(120, new Random());
gen.SetSerialNumber(SN);
gen.SetSubjectDN(CN);
gen.SetIssuerDN(CN);
gen.SetNotAfter(DateTime.MaxValue);
gen.SetNotBefore(DateTime.Now.Subtract(new TimeSpan(7, 0, 0, 0)));
gen.SetSignatureAlgorithm("MD5WithRSA");
gen.SetPublicKey(keypair.Public);
gen.AddExtension(X509Extensions.SubjectKeyIdentifier, false,
new SubjectKeyIdentifierStructure(keypair.Public));
var newCert = gen.Generate(keypair.Private);
return new X509Certificate2(DotNetUtilities.ToX509Certificate((Org.BouncyCastle.X509.X509Certificate)newCert));
}
When I'm starting the server it crashes with following exception:
ArgumentException: It is likely that certificate 'CN=MyCert' may not
have a private key that is capable of key exchange or the process may not have
access rights for the private key. Please see inner exception for detail.
The inner exception is null.
What did I do wrong?
Key pairs used for certificate exchange need to be created explicitly for that purpose. I think you just need to add another extension to your certificate generator like this:
gen.AddExtension(
X509Extensions.KeyUsage,
true,
new KeyUsage(KeyUsage.keyCertSign));
Not sure if that syntax is exactly correct but that's the idea: you need to make the key in the certificate as being for certificate signing, and that is a "critical" extension if present.