Can ServiceStack routes not handle special characters in their values? - asp.net

I'm developing an API for our business.
Requests should require authTokens that require a POST http verb to retrieve. The flow should work like this-
User's client POSTS username and password (ssl protected) to the GetAuthToken service. Service returns auth token.
User's client can use token in any other request, no longer requiring POST.
I've written a convenience rest function, CheckAuthToken, to allow users to debug whether their working auth token is correct. It requires the email address of the user and the auth token to check. This works fine in SOAP and via POST, but the route doesn't seem to work via GET.
Here's the DTO:
[Route("/auth/CheckAuthToken/{EmailAddress}/{AuthToken}")]
public class CheckAuthToken
{
public string EmailAddress { get; set; }
public string AuthToken { get; set; }
}
If I request, for example (note the %2E, at the recommendation of this post: ServiceStack Handler Not Found When Periods Present in Path):
GET /auth/CheckAuthToken/aaron%40meemailsite%2Ecom/Hnot0real0auth0token4mzSBhKwFXY6xQcgX6XqsE%3D HTTP/1.1\r\n
I still get a 404. It seems like the period is being decoded before handing off to ServiceStack.
I recognize that I could use query string variables instead, making my request this:
GET /auth/CheckAuthToken?EmailAddress=aaronb%40bluebookinc%2Ecom&AuthToken=HwjQoKiHD2HSngFeeCH1k4mzSBhKwFXY6xQcgX6XqsE%3D HTTP/1.1\r\n
But, I was hoping to be more flexible than that, especially in my business layer REST services, which will also require user identification via email on certain functions.
Any ideas?

What version of ServiceStack are you running?
This is a passing test in the latest version (3.9.55). I also tested with a simple API with your endpoints and was having no problems passing those values in the url.
[Route("/auth/CheckAuthToken/{EmailAddress}/{AuthToken}")]
public class CheckAuthToken : IReturn
{
public string EmailAddress { get; set; }
public string AuthToken { get; set; }
}
[Test]
public void Test()
{
var url = new CheckAuthToken() {
EmailAddress = "me#test.com",
AuthToken = "fake"
}.ToUrl("GET");
Assert.That(url, Is.EqualTo("/auth/CheckAuthToken/me%40test.com/fake"));
}

Related

Assign Delegated permission for System Assigned Managed Identity to Graph Api

I am trying to setup Managed Identity (system assigned), to assign delegated permission (like Tasks.ReadWrite) and then to use it to call Graph Api.
I have identified object Id using following code:
$app = Get-AzureADServicePrincipal -All $true -Filter "AppId eq '00000003-0000-0000-c000-000000000000'"
$role = $app.Oauth2Permissions | where-Object { $_.AdminConsentDisplayName -eq "Create, read, update, and delete user’s tasks and task lists" }
but when I run following command:
New-AzureADServiceAppRoleAssignment -Id $role.Id -ObjectId $miObjectID -PrincipalId $miObjectID -ResourceId $app.ObjectId
where $miObjectID is my managed identity Id, I am getting following error message:
New-AzureADServiceAppRoleAssignment : Error occurred while executing NewServicePrincipalAppRoleAssignment
Code: Request_BadRequest
Message: Permission being assigned was not found on application
I also tried to do the same. My understanding is, that this is not intended to be.
Typically, user-delegated permissions are supposed to be used interactively, involving user interaction, e.g. on a web-app accessing user resources on his behalf. If, like me, you're developing a backend service, this won't fly. For this scenario the supported way is to just use application permissions and be done with it.
However, imho application permissions are often too broad, granting access to ressources that are irrelevant. E.g. if your app needs to have access to one specific Sharepoint site, you have to authorize access for all sites in your tenant. Application permission cannot be scoped.
Due to compliance reasons this is not acceptable. Especially if you work in a LOB org.
Still, I did find a workaround to have the best of both worlds, i.e. having really scoped permissions and have the ability to leverage these unattended in a backend service. But there is one caveat: I did not get it working with a managed identity I had to use a regular service principal. If that's a compromise you can accept, the following may be helpful to you.
Dedicate a user principal for this scenario. Authorise that user as needed. Choose a password with max length, i.e. 256 chars. Enable MFA.
Create an app/service principal in Azure AD, generate client/app credentials
Create a demo web-app locally using available templates and the MSAL lib. Have the above app request the required user-delegated permissions from the user
Then, in the app code, use the Resource-owner password credential, ROPC flow to authenticate the app and assume the permissions from the user
public class RessourceOwnerPasswordCredentialFlow
{
public static async Task<AccessToken> GetToken(HttpClient http, string credJson, Guid tenantId)
{
var ropc = credJson.Deserialize<GraphROPC>();
var dict = new Dictionary<string, string>();
dict.Add("grant_type", "password"); // ROPC
dict.Add("username", ropc.User);
dict.Add("password", ropc.Password);
dict.Add("client_id", ropc.ClientId);
dict.Add("client_secret", ropc.ClientSecret);
dict.Add("scope", ".default");
var url = $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token";
var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = new FormUrlEncodedContent(dict) };
var res = await http.SendAsync(req);
var content = await res.Content.ReadAsStreamAsync();
var authResp = await content.DeserializeAsync<GraphAuthResponse>();
return new AccessToken(authResp.access_token, DateTimeOffset.UtcNow.AddSeconds(authResp.expires_in));
}
}
public class GraphROPC
{
public string User { get; set; }
public string Password { get; set; }
public string ClientId { get; set; }
public string ClientSecret { get; set; }
}
public class GraphAuthResponse
{
public string token_type { get; set; }
public string scope { get; set; }
public int expires_in { get; set; }
public string access_token { get; set; }
}
Note: Using the ROPC is not recommended by Microsoft.
However, I have found most of the objections to be not applicable in my case.
There is really no alternative to use delegated-permissions within a backend service app
I do have absolute trust in the app, because it's developed by me
The app does have to know the user credentials, these are stored in a KeyVault to which only the app has access (this time using MSI)
Also, one could argue that using two sets of credentials, user credentials plus app credentials is superior to using just the client credentials one would use if working with (more excessive) application permissions.
The ROPC doc says, that user accounts with MFA are not supported. However, I can confirm it is possible to work-around this restriction if conditional access policies are used. In our case the app has a fixed outbound public IP address which can be added as a trusted location. In fact, if the ability to whitelist a trusted location from the MFA requirement were missing, this would have been a blocker for the above steps.

Remove User from Directory Role using Graph API

I am trying to add and remove users from a directory role (Guest Inviter) based on a user's ID. My client ID has Directory.AccessAsUserAll for the Microsoft Graph application. I am using the ID for the directory role and the ID for the user. Using an HTTP Client call (verb is DELETE) I use the format suggested by Microsoft and get an "Insufficient privileges to complete the operation." error. I can perform other functions successfully
It seems clear to me that I am missing something. I would think that you still log in with the Client ID and Client Secret then do something with an admin type id and password rather than just create a new token based these credentials (because then why would you link them) similar to impersonation code but I don't know how and cannot seem to find an example of how.
Using HTTPClient
Verb DELETE
following this pattern
DELETE /directoryRoles/{id}/members/{id}/$ref
https://learn.microsoft.com/en-us/graph/api/directoryrole-delete-member?view=graph-rest-1.0&tabs=cs
Using C# creating bearer token (with client id and client secret) then using an HTTPCLient I call DeleteAsync using a url string based on the recommended pattern.
I see references to needing to pass user credential for a user in an admin role.
I think the issue is the absence of something important. This is called once the bearer token is obtained using client id and client secret for out tenant.
string delURL = $"{settings.RestUrl.value}{settings.RestVersion.value}/directoryRoles/{settings.GuestInviterRoleObjectID.value}/members/{user.id}/$ref";
HttpResponseMessage payload = await client.DeleteAsync(delURL);
Task<string> json = payload.Content.ReadAsStringAsync();
JObject o = new JObject();
if (json.Result.Length > 0)
{
o = JObject.Parse(json.Result);
}
I would like to remove the user from the Guest Inviter directory role. I get however
error: code:"authorization_requestDenied",
messsage: "Insufficient privileges to complete the operation" ....
Update: I was following this example https://dzone.com/articles/getting-access-token-for-microsoft-graph-using-oau-2
I built a class to contain the properties so after getting my original token using Client ID and Client secret then feeding in what I was told was a global admin credentials and now I get a 401 unauthorized error.
string tURL = $"https://login.microsoftonline.com/{settings.TenantID.value}/oauth2/token";
using (System.Net.WebClient c = new System.Net.WebClient())
{
c.Headers["Authorization"] = $"Bearer {token}";
c.Headers["Content-Type"] = "application/x-www-form-urlencoded";
System.Collections.Specialized.NameValueCollection data = new System.Collections.Specialized.NameValueCollection();
body.GetType().GetProperties().ToList().ForEach(delegate (System.Reflection.PropertyInfo item)
{
data.Add(item.Name, item.GetValue(body) == null ? string.Empty : item.GetValue(body).ToString());
});
var res = await Task.Run(() => c.UploadValues(tURL, data));
Task.WaitAll();
if(res != null)
{
string response = System.Text.Encoding.UTF8.GetString(res);
}
}
Data object
public class JSONBody
{
public string grant_type { get; set; }
public string client_id { get; set; }
public string client_secret { get; set; }
public string resource { get; set; }
public string username { get; set; }
public string password { get; set; }
public JSONBody()
{
this.grant_type = "password";
this.resource = "https://graph.microsoft.com";
}
}
I cannot prove or disprove the 401 error because I cannot prove my code works (or doesn't).
According to the documentation https://learn.microsoft.com/en-us/graph/api/directoryrole-delete-member
You’ll need an application with the delegated Directory.AccessAsUser.All permission. The you’ll need an admin to login to that application (with the correct permissions).
The application credentials (or client credentials flow) is unsupported, by design.
This could result in privilege elevation, if some admin would create an application with these permissions. If that admin would then be removed from the admin role he would be able to use his application to make himself admin again

aspnetboilerplate: SignalR JWT Authentication

We are trying to integrate SignalR in an 3rd party application to talk to our Hubs we have for our aspnetboilerplate application. This is using the .NET Core template. We are having an issue with the session in aspnetboilerplate having a null UserId even when getting past the attribute on our Hub to check for authorization.
The issue we are having is at random times the UserId inside of AbpSession will just be null. It gets past the [Authorize] attribute but aspnetboilerplate seems to think the UserId is null at random times. I can invoke a method on our Hub and see the UserId is correct for that user. Then the very next time I invoke that same method on the hub with the same user the UserId inside of AbpSession is null. I can then invoke the method again and the UserId will sometimes be null or sometimes be correct. Their doesn't seem to be any consistency in this issue. Every now and then it will alternate between being null and having the correct UserId.
Our client code:
let connection = new signalR.HubConnectionBuilder()
.withUrl('ENTER HUB URL HERE',
{
transport: signalR.HttpTransportType.LongPolling,
accessTokenFactory: () => {
return 'BEARER TOKEN HERE'
}}).build()
connection.invoke('sendGroupMessage', text, hardCodedChatGroup)
Here is a sample of our SignalR Hub on the server:
[AbpMvcAuthorize]
public class OpenChatHub : Hub, ITransientDependency
{
public IAbpSession AbpSession { get; set; }
public ILogger Logger { get; set; }
public OpenChatHub()
{
AbpSession = NullAbpSession.Instance;
Logger = NullLogger.Instance;
}
public async Task SendGroupMessage(string message, string groupName)
{
// logic for the SendGroupMessage would be here
var msg = new
{
sendById = AbpSession.UserId, // this will be null at random times
message = message
};
await Clients.Group(group).SendAsync("receiveChatMessage", msg);
}
}
I can view the requests for SignalR negotiating and communicating with the Hub and I can see the token being passed correctly each time.
After doing a bit more research on this while trying to get a test project together that I could put on GitHub to reproduce the issue I did end up solving the issue.
Using the following inside of our Hub gives us the correct UserId each time now. Context.User.Identity.GetUserId();
I believe this must be a bug inside of aspnetboilerplate now. I will be trying to get an issue reported on the GitHub.

Adding security to RESTful API [closed]

Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
We don’t allow questions seeking recommendations for books, tools, software libraries, and more. You can edit the question so it can be answered with facts and citations.
Closed 6 years ago.
Improve this question
I am wanting to implement two websites that need to communicate with each other. (Since one of the sites has a separate deployment for each customer, and is spread across many servers, sharing a database or communicating privately is not an option.) So I've been looking into RESTful APIs.
Unfortunately, I'm running into a lot of information that I'm not familiar with. One issue is security. We don't need anything fancy--we're not a bank or anything. I think we can just get away with HTTPS and a basic username and password.
Questions:
How would I pass the username and password to the API? Would they just be passed as bare arguments in the URL?
Does .NET provide any mechanism for authorizing such username and passwords, or do I just manually see if the password is in our database on each and every request? (I would hash for security.)
How would I pass the username and password to the API? Would they just
be passed as bare arguments in the URL?
It can be either in the URL or in the header. If you are using HTTPS, it will all be encrypted so it will not be bare. Please see this for more details.
Does .NET provide any mechanism for authorizing such username and
passwords, or do I just manually see if the password is in our
database on each and every request? (I would hash for security.)
No you do not need to check the database on every request. You can check once, create a token with an expiry and the client can keep sending you the token. This way you do not have to keep checking the database every single time.
Please see see this answer for some helpful information.
I think basic authentication with base64 encoding will be sufficient. If not you can always change it. Here are the different ways to apply it to your backend code:
To apply an authentication filter to a controller, decorate the controller class with the filter attribute. The following code sets the [IdentityBasicAuthentication] filter on a controller class, which enables Basic Authentication for all of the controller's actions.
[IdentityBasicAuthentication] // Enable Basic authentication for this controller.
[Authorize] // Require authenticated requests.
public class HomeController : ApiController
{
public IHttpActionResult Get() { . . . }
public IHttpActionResult Post() { . . . }
}
To apply the filter to one action, decorate the action with the filter. The following code sets the [IdentityBasicAuthentication] filter on the controller's Post method.
[Authorize] // Require authenticated requests.
public class HomeController : ApiController
{
public IHttpActionResult Get() { . . . }
[IdentityBasicAuthentication] // Enable Basic authentication for this action.
public IHttpActionResult Post() { . . . }
}
To apply the filter to all Web API controllers, add it to GlobalConfiguration.Filters.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Filters.Add(new IdentityBasicAuthenticationAttribute());
// Other configuration code not shown...
}
}
Finally here is an example of the implementation, you may change it as you need:
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Principal;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Filters;
using BasicAuthentication.Results;
namespace BasicAuthentication.Filters
{
public abstract class BasicAuthenticationAttribute : Attribute, IAuthenticationFilter
{
public string Realm { get; set; }
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
HttpRequestMessage request = context.Request;
AuthenticationHeaderValue authorization = request.Headers.Authorization;
if (authorization == null)
{
// No authentication was attempted (for this authentication method).
// Do not set either Principal (which would indicate success) or ErrorResult (indicating an error).
return;
}
if (authorization.Scheme != "Basic")
{
// No authentication was attempted (for this authentication method).
// Do not set either Principal (which would indicate success) or ErrorResult (indicating an error).
return;
}
if (String.IsNullOrEmpty(authorization.Parameter))
{
// Authentication was attempted but failed. Set ErrorResult to indicate an error.
context.ErrorResult = new AuthenticationFailureResult("Missing credentials", request);
return;
}
Tuple<string, string> userNameAndPasword = ExtractUserNameAndPassword(authorization.Parameter);
if (userNameAndPasword == null)
{
// Authentication was attempted but failed. Set ErrorResult to indicate an error.
context.ErrorResult = new AuthenticationFailureResult("Invalid credentials", request);
return;
}
string userName = userNameAndPasword.Item1;
string password = userNameAndPasword.Item2;
IPrincipal principal = await AuthenticateAsync(userName, password, cancellationToken);
if (principal == null)
{
// Authentication was attempted but failed. Set ErrorResult to indicate an error.
context.ErrorResult = new AuthenticationFailureResult("Invalid username or password", request);
}
else
{
// Authentication was attempted and succeeded. Set Principal to the authenticated user.
context.Principal = principal;
}
}
protected abstract Task<IPrincipal> AuthenticateAsync(string userName, string password,
CancellationToken cancellationToken);
private static Tuple<string, string> ExtractUserNameAndPassword(string authorizationParameter)
{
byte[] credentialBytes;
try
{
credentialBytes = Convert.FromBase64String(authorizationParameter);
}
catch (FormatException)
{
return null;
}
// The currently approved HTTP 1.1 specification says characters here are ISO-8859-1.
// However, the current draft updated specification for HTTP 1.1 indicates this encoding is infrequently
// used in practice and defines behavior only for ASCII.
Encoding encoding = Encoding.ASCII;
// Make a writable copy of the encoding to enable setting a decoder fallback.
encoding = (Encoding)encoding.Clone();
// Fail on invalid bytes rather than silently replacing and continuing.
encoding.DecoderFallback = DecoderFallback.ExceptionFallback;
string decodedCredentials;
try
{
decodedCredentials = encoding.GetString(credentialBytes);
}
catch (DecoderFallbackException)
{
return null;
}
if (String.IsNullOrEmpty(decodedCredentials))
{
return null;
}
int colonIndex = decodedCredentials.IndexOf(':');
if (colonIndex == -1)
{
return null;
}
string userName = decodedCredentials.Substring(0, colonIndex);
string password = decodedCredentials.Substring(colonIndex + 1);
return new Tuple<string, string>(userName, password);
}
public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
Challenge(context);
return Task.FromResult(0);
}
private void Challenge(HttpAuthenticationChallengeContext context)
{
string parameter;
if (String.IsNullOrEmpty(Realm))
{
parameter = null;
}
else
{
// A correct implementation should verify that Realm does not contain a quote character unless properly
// escaped (precededed by a backslash that is not itself escaped).
parameter = "realm=\"" + Realm + "\"";
}
context.ChallengeWith("Basic", parameter);
}
public virtual bool AllowMultiple
{
get { return false; }
}
}
}
If you still want to read more then here is a great article which goes into details. I have copied the above code from this article. It has lots of great information.
If you control or exert significant influence on both sides of the connection, client ssl certificates is a really strong and powerful way of doing this. It's attractive to me in this case because it only requires distributing a trusted CA certificate which can be done before the client certificates are created. It's far more secure than any username and password could ever be ( because the password doesn't need to go across the wire).
Any other solution with authentication I can think of, you're going to have to have some sort of data source to verify the credentials. But x509 solves this problem for you. We've done it at work between applications and other than managing the certificates it works really, really well. And it's basically the most secure thing available.
I don't know much about .net in general, but ( not to lmgtfy ) https://support.microsoft.com/en-us/kb/315588 seems like the step by step format you are looking for.
Just a thought, and it really depends on what you meant by "username/password". If this means "authorization"/access to some API call and you want to ensure that the client is "authorized" to make a call to your API (only apps A, B can make api calls to API - and it seems this is what you're looking for based on your comment above):
As in the comment above, authorization header, using JWT. There is an great/easy JWT library in Nuget
it's pretty much something like a "shared secret" used to sign a "payload" (the JWT)
the "sender" will build the JWT and sign it (and add to header or whatever protocol you want - it can be body if prefer it over headers)
the "receiver" will verify the JWT sent
this includes handling/mitigating "replays" - the JWT spec has an "expire" field (exp) that you can have the library validate as well (or not, it's up to you)
The project site is on Github with samples.
Hth.

Passing the username/password from client to web API using GET

for example I have a web API : http://example.com/api/product.
I have a C# client to consume this web API. Something like that to get whole list of product.
// List all products.
HttpResponseMessage response = client.GetAsync("api/products").Result; // Blocking call!
if (response.IsSuccessStatusCode)
{
// Parse the response body. Blocking!
var products = response.Content.ReadAsAsync<IEnumerable<Product>>().Result;
foreach (var p in products)
{
Console.WriteLine("{0}\t{1};\t{2}", p.Name, p.Price, p.Category);
}
}
else
{
Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase);
}
How do I pass the username and password from C# client to server's API? What I want is when the C# client to get whole product list from web API.
The client will send the username and password to the server's API. if the server's web API checks whether it is authorized user from database, if not don't let it get product list.
I used the following approach in a proof of concept some time ago, I hope it helps you.
I wrote something like this, an "AuthenticationController" with 2 methods:
public bool Login(string username, string password, bool rememberMe)
{
if (Membership.ValidateUser(username, password))
{
FormsAuthentication.SetAuthCookie(username, rememberMe);
return true;
}
return false;
}
public void Logout()
{
FormsAuthentication.SignOut();
}
The Login method creates a cookie that will be sent to the client; then, in each request, you need to send it back to the server. You can use the [Authorize] attribute in your controller actions to validate allowed roles and rights.
My recommendation is to use have an authentication routine that will assign a token to the client. The client would then cache that token and pass that token in subsequent requests. The authentication routine should be via SSL to prevent sniffing on the wire and shouldn't be stored on the device at all (the token can be cached to the device).
This will give you a fair bit of control over the client. Your service is then in a position where it can preemptively deactivate the client (kill the token and force a re-auth - essentially a timemout situation). You are also in a position to protect your application on the client (if the application is compromised on the device the user credentials won't be passed around).
You could use DotNetOpenAuth to get you started along this path.
[System.Web.Mvc.AcceptVerbs(HttpVerbs.Post)]
public ActionResult LogOn(string loginIdentifier)
{
if (!Identifier.IsValid(loginIdentifier))
{
ModelState.AddModelError("loginIdentifier",
"The specified login identifier is invalid");
return View();
}
else
{
var openid = new OpenIdRelyingParty();
IAuthenticationRequest request = openid.CreateRequest(
Identifier.Parse(loginIdentifier));
// Require some additional data
request.AddExtension(new ClaimsRequest
{
BirthDate = DemandLevel.NoRequest,
Email = DemandLevel.Require,
FullName = DemandLevel.Require
});
return request.RedirectingResponse.AsActionResult();
}
}
Source: Sample Code

Resources