I'm trying to build a system working with ADFS and claims. At the moment, this is just a "toy" implementation.
I've built a very simple MVC web application, set it up using the "Identity and Access..." wizard in Visual Studio to talk to an ADFS 2.0 server, and deployed it to an IIS server. All works fine, and I can examine and list the received claims.
The next step is to build a Web API based REST service (representing back-end services that the MVC application is going to depend on), so I want to pass the credentials across to that back-end server so that it can make suitable authorization decisions.
So the first step is for me to create the delegation token (and I'll then, hopefully, work out what to do with it in terms of the HttpClient class to make the rest call). I've got this:
//We need to take the bootstrap token and create an appropriate ActAs token
var rst = new RequestSecurityToken
{
AppliesTo = new EndpointReference("https://other-iis.example.com/Rest"),
RequestType = RequestTypes.Issue,
KeyType = KeyTypes.Symmetric,
ActAs = new SecurityTokenElement(((BootstrapContext)((ClaimsIdentity)User.Identity).BootstrapContext).SecurityToken)
};
var sts = new SecurityTokenService(); //This line isn't valid
var resp = sts.Issue(System.Threading.Thread.CurrentPrincipal as ClaimsPrincipal, rst);
But, the issue is that SecurityTokenService is abstract. I can't find any types derived from this class in either System.IdentityModel nor System.IdentityModel.Services, and the above doesn't include any reference to the ADFS server which I'll obviously need to provide at some point.
Of course, I may be going down completely the wrong route also, or am just hitting a minor stumbling block and not seeing a much larger one looming in the distance, so any advice on that would be appreciated also.
I've looked at, for example, Identity Delegation Scenario, but that uses CreateChannelActingAs, which I don't think is going to work when I'm talking to a rest service (or will it?), and also doesn't seem to apply to .NET 4.5.
I am requesting tokens from an ADFS 2.0 for caching and looking at the DisplayToken. Maybe this can help you get started.
Here is what I can up with:
public SecurityToken GetToken(out RequestSecurityTokenResponse rstr)
{
Console.WriteLine("Connecting to STS...");
WSTrustChannelFactory factory = null;
try
{
if (_useCredentials)
{
// use a UserName Trust Binding for username authentication
factory =
new WSTrustChannelFactory(
new UserNameWSTrustBinding(SecurityMode.TransportWithMessageCredential),
"https://<adfs>/adfs/services/trust/13/UsernameMixed");
factory.TrustVersion = TrustVersion.WSTrust13;
// Username and Password here...
factory.Credentials.UserName.UserName = "username";
factory.Credentials.UserName.Password = "password";
}
else
{
// Windows authentication over transport security
factory = new WSTrustChannelFactory(
new WindowsWSTrustBinding(SecurityMode.Transport),
"https://<adfs>/adfs/services/trust/13/windowstransport") { TrustVersion = TrustVersion.WSTrust13 };
}
var rst = new RequestSecurityToken
{
RequestType = RequestTypes.Issue,
AppliesTo = SvcEndpoint,
KeyType = KeyTypes.Symmetric,
RequestDisplayToken = true
};
Console.WriteLine("Creating channel for STS...");
IWSTrustChannelContract channel = factory.CreateChannel();
Console.WriteLine("Requesting token from " + StsEndpoint.Uri);
SecurityToken token = channel.Issue(rst, out rstr);
Console.WriteLine("Received token from " + StsEndpoint.Uri);
return token;
}
finally
{
if (factory != null)
{
try
{
factory.Close();
}
catch (CommunicationObjectFaultedException)
{
factory.Abort();
}
}
}
}
You might have to acivate the UsernameMixed Endpoint in your ADFS 2.0 if you want to use it and don't forget to restart the service afterwards!
From msdn
To create an STS you must derive from the SecurityTokenService class. In your custom class you must, at a minimum, override the GetScope and GetOutputClaimsIdentity methods.
Not sure how much this will help you, but You're not supposed to create a SecurityTokenService. You are not creating a new token here, and your aplication is not supposed to act as the STS - this is what the AD FS is for.
Your application should only delegate the token received from the AD FS to the service (the concept is described in the link from msdn you provided in your question)
Im guessing theres a good chance the web api will suppor this as well, as its built upon wcf, and from the http point of view - theres no reason it wont support a ws-federation/saml 2 tokens.
EDIT:
This video (starting at 35:00+-) shows a way, i think, to implement what youre looking for, with ws-federation saml token. im guessing its also possible with a saml2 token
Related
I'm kind of new to the whole WCF and SOAP topic so please be kind.
I'm using a generated SOAP Client with .net6. In another project we successfully worked with the same Web Service using the old .net Framework 2.0 Web References and the same credentials.
Strange enough everything seemed to work fine at first. Until I realized, that it does not use the given credentials to authenticate. Instead it authenticates with my own domain user.
I also tried to get it to work with explicitly setting the binding with a BasicHttpBinding but I only could get the same broken logic to work or I got various authentication/protocol/security errors.
So it seems the authentication is basically working. It just doesn't use the provided credentials. So my question is: How can I configure it to work with the provided identity?
I also found out that it might have anything to do with a cached Windows token. But how can I get rid of it. How to prevent caching in the first place?
EDIT:
Specified the variable types explicitly.
string url = "http://someServer/AdministrationService.asmx";
AdministrationServiceSoapClient client = new AdministrationServiceSoapClient(
AdministrationServiceSoapClient.EndpointConfiguration.AdministrationServiceSoap,
url);
WindowsClientCredential credential = client.ClientCredentials.Windows;
credential.ClientCredential.UserName = "username";
credential.ClientCredential.Password = "password";
credential.ClientCredential.Domain = "DOMAIN";
GetServerInfoRequest getServerInfoRequest = new GetServerInfoRequest
{
// some stuff set here
};
GetServerInfoRequest getServerInfoReply = await client.GetServerInfoAsync(getServerInfoRequest);
As far as I know, BasicHttpBinding has security disabled by default, but can be added setting the BasicHttpSecurityMode to a value other than None in the constructor. It can be configured according to the instructions in BasicHttpBinding and BasicHttpBinding Constructors.
By default, setting up client credentials involves two steps: determining the type of client credential required by the service and specifying an actual client credential, as described in this document.
After waiting a day it is working. It seems that the cached credentials became invalid somehow.
Strange enough the simple service creation from above is not working anymore. Instead I have to use the following.
var client = new AdministrationServiceSoapClient(
new BasicHttpBinding()
{
Security = new BasicHttpSecurity()
{
Mode = BasicHttpSecurityMode.TransportCredentialOnly,
Message = new BasicHttpMessageSecurity()
{
ClientCredentialType = BasicHttpMessageCredentialType.UserName,
},
Transport = new HttpTransportSecurity()
{
ClientCredentialType = HttpClientCredentialType.Windows,
ProxyCredentialType = HttpProxyCredentialType.Windows,
}
},
},
new EndpointAddress(url));
I went through the guide of for getting setup to call the new SP-API (https://github.com/amzn/selling-partner-api-docs/blob/main/guides/developer-guide/SellingPartnerApiDeveloperGuide.md), and during the process checked off all of the api areas to grant access to (i.e. Orders, Inventory, etc). I am using the C# library provided by Amazon (https://github.com/amzn/selling-partner-api-models/tree/main/clients/sellingpartner-api-aa-csharp). I successfully get an access token and successfully sign the request, but always get the following error:
Access to requested resource is denied. / Unauthorized, with no details.
I am trying to perform a simple get to the /orders/v0/orders endpoint. What am I doing wrong?
Below is my code:
private const string MARKETPLACE_ID = "ATVPDKIKX0DER";
var resource = $"/orders/v0/orders";
var client = new RestClient("https://sellingpartnerapi-na.amazon.com");
IRestRequest restRequest = new RestRequest(resource, Method.GET);
restRequest.AddParameter("MarketPlaceIds", MARKETPLACE_ID, ParameterType.QueryString);
restRequest.AddParameter("CreatedAfter", DateTime.UtcNow.AddDays(-5), ParameterType.QueryString);
var lwaAuthorizationCredentials = new LWAAuthorizationCredentials
{
ClientId = AMAZON_LWA_CLIENT_ID,
ClientSecret = AMAZON_LWA_CLIENT_SECRET,
RefreshToken = AMAZON_LWA_REFRESH_TOKEN,
Endpoint = new Uri("https://api.amazon.com/auth/o2/token")
};
restRequest = new LWAAuthorizationSigner(lwaAuthorizationCredentials).Sign(restRequest);
var awsAuthenticationCredentials = new AWSAuthenticationCredentials
{
AccessKeyId = AMAZON_ACCESS_KEY_ID,
SecretKey = AMAZON_ACCESS_SECRET,
Region = "us-east-1"
};
restRequest = new AWSSigV4Signer(awsAuthenticationCredentials).Sign(restRequest, client.BaseUrl.Host);
var response = client.Execute(restRequest);
If you followed the SP-API guide, then you created a Role (which is the IAM ARN your app is registered with) and a User which has permissions to assume that role to make API calls.
However, one thing the guide is not clear about is that you can't make API calls using that user's credentials directly. You must first call the STS API's AssumeRole method with your User's credentials (AMAZON_ACCESS_KEY_ID/AMAZON_ACCESS_SECRET), and it will return temporary credentials authorized against the Role. You use those temporary credentials when signing requests.
AssumeRole will also return a session token which you must include with your API calls in a header called X-Amz-Security-Token. For a brief description of X-Amz-Security-Token see https://docs.aws.amazon.com/STS/latest/APIReference/CommonParameters.html
You also get this error if your sp app is under review, drove me nuts!
If you using c# take look to
https://github.com/abuzuhri/Amazon-SP-API-CSharp
AmazonConnection amazonConnection = new AmazonConnection(new AmazonCredential()
{
AccessKey = "AKIAXXXXXXXXXXXXXXX",
SecretKey = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
RoleArn = "arn:aws:iam::XXXXXXXXXXXXX:role/XXXXXXXXXXXX",
ClientId = "amzn1.application-XXX-client.XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
ClientSecret = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
RefreshToken= "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
});
var orders= amazonConnection.Orders.ListOrders();
In our situation, we had to explicitly add an IAM policy to the user we defined as making the API call. Please see the link below and confirm that the user you have calling the API has the policy assigned to them:
https://github.com/amzn/selling-partner-api-docs/blob/main/guides/developer-guide/SellingPartnerApiDeveloperGuide.md#step-3-create-an-iam-policy
Somehow we went through the step-by-step setup twice, and adding this explicit policy was missed. Initially I believe it was added 'inline' as instructed, but that does not seem to work.
I dont think is a duplicated question, buy the solution may apply: https://stackoverflow.com/a/66860192/1034622
I downloaded the example below to get an access token from MS Graph and it worked fine. Now I changed the code to get a token from a custom web API. On apps.dev.microsoft.com I registered a client application and an the API.
Client and server registration in AD
private static async Task<AuthenticationResult> GetToken()
{
const string clientId = "185adc28-7e72-4f07-a052-651755513825";
var clientApp = new PublicClientApplication(clientId);
AuthenticationResult result = null;
string[] scopes = new string[] { "api://f69953b0-2d7f-4523-a8df-01f216b55200/Test" };
try
{
result = await clientApp.AcquireTokenAsync(scopes, "", UIBehavior.SelectAccount, string.Empty);
}
catch (Exception x)
{
if (x.Message == "User canceled authentication")
{
}
return null;
}
return result;
}
When I run the code I login to AD via the dialog en get the following exception in the debugger:
Error: Invalid client Message = "AADSTS65005: The application
'CoreWebAPIAzureADClient' asked for scope 'offline_access' that
doesn't exist on the resource. Contact the app vendor.\r\nTrace ID:
56a4b5ad-8ca1-4c41-b961-c74d84911300\r\nCorrelation ID:
a4350378-b802-4364-8464-c6fdf105cbf1\r...
Error message
Help appreciated trying for days...
For anyone still striking this problem, please read this:
https://www.andrew-best.com/posts/please-sir-can-i-have-some-auth/
You'll feel better after this guy reflects all of your frustrations, except that he works it out...
If using adal.js, for your scope you need to use
const tokenRequest = {
scopes: ["https://management.azure.com/user_impersonation"]
};
I spent a week using
const tokenRequest = {
scopes: ["user_impersonation"]
};
.. since that is the format that the graph API scopes took
As of today, the V2 Endpoint does not support API access other than the Microsoft Graph. See the limitations of the V2 app model here.
Standalone Web APIs
You can use the v2.0 endpoint to build a Web API that is secured with
OAuth 2.0. However, that Web API can receive tokens only from an
application that has the same Application ID. You cannot access a Web
API from a client that has a different Application ID. The client
won't be able to request or obtain permissions to your Web API.
For the specific scenario that you are trying to accomplish, you need to use the V1 App Model (register apps on https://portal.azure.com).
In the very near future, V2 apps will be enabled to call other APIs other than Microsoft Graph, so your scenario will be supported, but that is just not the case today. You should keep an eye out on our documentation for this update.
In your (server) application registration in AAD, you need to specify your scopes in the oauth2Permissions element.
You may already have a user_impersonation scope set. Copy that as a baseline, give it a unique GUID and value, and then AAD will let your client request an access token with your new scope.
I have implemented the authentication with OWIN and bearer token and it works fine when user tries to login. So I go to the special \Token url and provide username/password and get this token in response.
But I have a register user functionality as well and ideally I would like to get this token right away after I do register a user. I can go to the \Token internally in a web server, or do \Token after registration on the client, but I hope there is a solution where I can do something like
public Token RegisterUser(RegisterUserRequest request)
{
// Registration here
return OAuthProvider.GenerateToken(username, password)
}
Is it possible to do something like that?
This took ages for me to figure out so I thought I'd add it here.
You can manually generate a token using the Protect function on AccessTokenFormat of your OAuthAuthorizationServerOptions object, assuming you've made it static in your startup class as otherwise it will be null.
AuthenticationTicket ticket = new AuthenticationTicket(claimsIdentity, new AuthenticationProperties());
string token = Startup.OAuthServerOptions.AccessTokenFormat.Protect(ticket);
Seems a very odd place to put the token generation logic.
Anyway you can then do with the token as you wish, likely attach it to your outgoing header or response object.
I was looking for the same thing you were, but I couldn't find anything, so I just made the request internally. Not very much work, but still a little disappointing.
public JToken GetLoginResponse(UserSignupDto dto)
{
var tokenPath = Request.RequestUri.GetLeftPart(UriPartial.Authority) + "/token";
var reqData = string.Format("grant_type=password&username={0}&password={1}&client_id={2}", dto.Email, dto.Password, dto.ClientId);
using(var client = new WebClient())
{
return JObject.Parse(client.UploadString(tokenPath, reqData));
}
}
This question has been discussed in several topics here but I could not find the answer for me.
What I'm trying to do is use an IP camera through the Onvif interface. I've generated the web services from the WSDL files available in the Onvif homepage, and added the custom SOAP authentication code as suggested here, and I am able to retrieve the device capabilities etc. etc.
But for some services, e.g, PTZ control, also HTTP authentication is needed. My code removes the ClientCredentials behaivor (so yeah, I guess setting them does not make any sense, but I still left those lines in hope that maybe the HTTP transport would try to use them):
HttpTransportBindingElement httpBindingElement = new HttpTransportBindingElement();
httpBindingElement.AuthenticationScheme = AuthenticationSchemes.Basic;
...
PTZClient ptzClient = new PTZClient(customBinding, endPointAddress);
ptzClient.Endpoint.Behaviors.Remove(typeof(System.ServiceModel.Description.ClientCredentials));
UsernameClientCredentials onvifCredentials = new UsernameClientCredentials(new UsernameInfo(_username, _password));
ptzClient.Endpoint.Behaviors.Add(onvifCredentials);
ptzClient.ClientCredentials.UserName.UserName = _username;
ptzClient.ClientCredentials.UserName.Password = _password;
Still when I look at wireshark, i see that the SOAP authentication is generated but no HTTP authentication header is set (well, I already expected that since i have a custom behaivor here). So the question is, if I am creating the binding this way, what are my best options to add HTTP authentication headers? Can I just add a message inspector, and if so, any examples? Must I create a different transport binding? I've seen people advising others to use BasicHttpBinding and then setting the Security property on that, but where do the credentials go in that case and how do I apply the BasicHttpBinding instance to my binding? Are there any callbacks in the WCF that get triggered by the HTTP 401 code that i can hook up to and then provide the header? This is actually my first experience with WCF and so far I've done everything from examples found in the internet, but as for this particular issue I haven't been able to find anything.
If anyone is interested this is how I got it working. I combined the BasicHttpBinding with the client credentials in a following way:
TransportSecurityBindingElement transportSecurity = new TransportSecurityBindingElement();
// UsernameCredentials is a class implementing WS-UsernameToken authentication
transportSecurity.EndpointSupportingTokenParameters.SignedEncrypted.Add(new UsernameTokenParameters());
transportSecurity.AllowInsecureTransport = true;
transportSecurity.IncludeTimestamp = false;
TextMessageEncodingBindingElement messageEncoding = new TextMessageEncodingBindingElement(MessageVersion.Soap12, Encoding.UTF8);
HttpClientCredentialType[] credentialTypes = new HttpClientCredentialType[3] { HttpClientCredentialType.None, HttpClientCredentialType.Basic, HttpClientCredentialType.Digest };
...
foreach (HttpClientCredentialType credentialType in credentialTypes)
{
BasicHttpBinding httpBinding = new BasicHttpBinding(BasicHttpSecurityMode.TransportCredentialOnly);
httpBinding.Security.Transport.ClientCredentialType = credentialType;
BindingElementCollection elements = new BindingElementCollection(new BindingElement[1]{messageEncoding});
foreach(BindingElement element in httpBinding.CreateBindingElements())
{
if (element is TextMessageEncodingBindingElement)
continue;
elements.Add(element);
}
CustomBinding customBinding = new CustomBinding(elements);
DeviceClient deviceClient = new DeviceClient(customBinding, endPointAddress);
if (credentialType == HttpClientCredentialType.Basic)
{
// Set all credentials, not sure from which one WCF actually takes the value
deviceClient.ClientCredentials.UserName.UserName = pair[0];
deviceClient.ClientCredentials.UserName.Password = pair[1];
}
else if (credentialType == HttpClientCredentialType.Digest)
{
deviceClient.ClientCredentials.HttpDigest.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Delegation;
deviceClient.ClientCredentials.HttpDigest.ClientCredential.UserName = pair[0];
deviceClient.ClientCredentials.HttpDigest.ClientCredential.Password = pair[1];
}
}
This works efficiently with a device for which we do not know the authentication mode and works on both (HTTP/SOAP) authentication level.
I detailed how HTTP digest works in another answer.
Remember that only functions of class PRE_AUTH, according to ยง5.12.1 of the Core spec, require authentication.
You should invoke a function of any class but PRE_AUTH without any form authentication. If you get a HTTP 401 then you have to use HTTP digset, otherwise you'll have to got with WS-UsernameToken.
You can't directly use HTTP digest because you'll need at least the device to send you the challange for HTTP digest.