This initial login succeeds:
public static MobileServiceClient MOBILE = new MobileServiceClient("https://myapp.azure-mobile.net/",myApplicationKey);
MobileServiceAuthenticationProvider GOOGLEPROVIDER = MobileServiceAuthenticationProvider.Google;
private async Task Connect() {
var USER = await MOBILE.LoginAsync(this, GOOGLEPROVIDER);
var CACHE = new Dictionary<string, string> { { "token", USER.MobileServiceAuthenticationToken } };
var ACCOUNT = new Account(USER.UserId, CACHE);
var STORE = AccountStore.Create(this);
STORE.Save(ACCOUNT, "Google");
}
but then this attempt to reuse the token to reconnect without a login page fails:
public async Task Reconnect() {
var STORE = AccountStore.Create(this);
var token = STORE.FindAccountsForService("Google").ToArray()[0].Properties["token"];
// token seems ok
var jsonToken = new JObject();
jsonToken.Add("access_token", token);
var USER = await MOBILE.LoginAsync(MobileServiceAuthenticationProvider.Google, jsonToken); // BOOM!
}
... with the following message: "The POST Google login request must contain both code and id_token in the body of the request."
What I am getting wrong here?
The token you use in the code, viz.
var CACHE = new Dictionary { { "token",USER.MobileServiceAuthenticationToken } };
The MobileServiceAuthenticationToken above is a token specific to MobileServices and cannot be used in the LoginAsync method (LoginAsync method requires a Google OAuth token.)
Please see this Get User Info from Google Api Using Azure Mobile Services for Xamarin Android
Related
Trying to allow a CLI I'm developing to "login" via web browser and obtain an access token for the user's account, similar to how gcloud and github's CLIs do it. I realize it'll be using the OAuth Authorization Code flow.
But what about the client_secret?
I've found out that github cli just doesn't care about exposing it, and it's right there in the source code: https://github.com/cli/cli/blob/6a8deb1f5a9f2aa0ace2eb154523f3b9f23a05ae/internal/authflow/flow.go#L25-L26
Why is this not a problem? or is it?
I'm not yet using OAuth for the cli's login
STANDARDS
The CLI app is a native public client and should use authorization code flow + PKCE rather than a fixed client secret. It should also follow the flow from
RFC8252 and receive the browser response using a local HTTP (loopback) URI.
THIS IMPLEMENTATION
Looks like the github code here uses a client secret and does not use PKCE. You may have to provide a client secret if using this library, but it cannot be kept secret from users. Any user could easily view it, eg with an HTTP proxy tool.
CODE
If the infrastructure enables you to follow the standards, aim for something similar to this Node.js code.
* The OAuth flow for a console app
*/
export async function login(): Promise<string> {
// Set up the authorization request
const codeVerifier = generateRandomString();
const codeChallenge = generateHash(codeVerifier);
const state = generateRandomString();
const authorizationUrl = buildAuthorizationUrl(state, codeChallenge);
return new Promise<string>((resolve, reject) => {
let server: Http.Server | null = null;
const callback = async (request: Http.IncomingMessage, response: Http.ServerResponse) => {
if (server != null) {
// Complete the incoming HTTP request when a login response is received
response.write('Login completed for the console client ...');
response.end();
server.close();
server = null;
try {
// Swap the code for tokens
const accessToken = await redeemCodeForAccessToken(request.url!, state, codeVerifier);
resolve(accessToken);
} catch (e: any) {
reject(e);
}
}
}
// Start an HTTP server and listen for the authorization response on a loopback URL, according to RFC8252
server = Http.createServer(callback);
server.listen(loopbackPort);
// Open the system browser to begin authentication
Opener(authorizationUrl);
});
}
/*
* Build a code flow URL for a native console app
*/
function buildAuthorizationUrl(state: string, codeChallenge: string): string {
let url = authorizationEndpoint;
url += `?client_id=${encodeURIComponent(clientId)}`;
url += `&redirect_uri=${encodeURIComponent(redirectUri)}`;
url += '&response_type=code';
url += `&scope=${scope}`;
url += `&state=${encodeURIComponent(state)}`;
url += `&code_challenge=${encodeURIComponent(codeChallenge)}`;
url += '&code_challenge_method=S256';
return url;
}
/*
* Swap the code for tokens using PKCE and return the access token
*/
async function redeemCodeForAccessToken(responseUrl: string, requestState: string, codeVerifier: string): Promise<string> {
const [code, responseState] = getLoginResult(responseUrl);
if (responseState !== requestState) {
throw new Error('An invalid authorization response state was received');
}
let body = 'grant_type=authorization_code';
body += `&client_id=${encodeURIComponent(clientId)}`;
body += `&redirect_uri=${encodeURIComponent(redirectUri)}`;
body += `&code=${encodeURIComponent(code)}`;
body += `&code_verifier=${encodeURIComponent(codeVerifier)}`;
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body,
};
const response = await fetch(tokenEndpoint, options);
if (response.status >= 400) {
const details = await response.text();
throw new Error(`Problem encountered redeeming the code for tokens: ${response.status}, ${details}`);
}
const data = await response.json();
return data.access_token;
}
Can you point me to the right way and set of parameters to:
Make an authorization request with PKCE to my identity endpoint (https://.../login) in Postman
In the attachments there is the list of parameters that send.
as grant_type value I use --> authorization_code
Unfortunately I get "bad request", Invalid_grant in Postman
make the access token request. In this request I get Invalid request. I guess I miss the parameter refresh token but I don't know how to get/generate it:
I wrote the code of the Azure function to request the Access Token, unfortunately I get {"error":"invalid_request"} from the token endpoint.
Here is my code, can you tell me what I am doing wrong?
[FunctionName("GetAccessToken")]
public async Task<IActionResult> GetAccessToken(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req,
ILogger log)
{
try
{
log.LogInformation("C# HTTP trigger function ''GetAccessToken'' processed a request.");
string clientSecret = "some secret";
string accessToken = "";
RequestAccessToken rT = new RequestAccessToken();
rT.Code = req.Form["code"];
rT.RedirectUri = req.Form["redirect_uri"];
rT.GrantType = req.Form["grant_type"];
rT.ClientId = req.Form["client_id"];
rT.CodeVerifier = req.Form["code_verifier"];
using (var client = new HttpClient())
{
client.BaseAddress = new Uri("https://<access_token_endpoint_base_uri>");
client.DefaultRequestHeaders
.Accept
.Add(new MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded"));//ACCEPT header
var body = new { EntityState = new {
code = rT.Code,
redirect_uri = rT.RedirectUri,
grant_type = rT.GrantType,
client_id = rT.ClientId,
client_secret = clientSecret,
code_verifier = rT.CodeVerifier
} };
var result = await client.PostAsJsonAsync(
"/login",
body);
accessToken = await result.Content.ReadAsStringAsync();
}
return new OkObjectResult(accessToken);
}
catch (Exception ex)
{
log.LogInformation(ex.ToString());
return new ObjectResult(ex.ToString()) { StatusCode = 500 };
}
}
Steps 4 and 8 of my blog post show the standard PKCE parameters.
It is tricky to reproduce the whole flow via a tool such as Postman though, since there is typically also the need to follow redirects and manage a form post of user + password.
We have a requirement to authenticate users in IdentityServer4 against an external API. The scenario works like this:
User visits a Javascript client application and clicks the login button to redirect to IdentityServer login page (exact same client as provided in the docs here
User enters their username (email) and password
IdentityServer4 connects to an external API to verify credentials
User is redirected back to the JavaScript application
The above process works perfect when using the TestUsers provided in the QuickStarts. However, when an API is used, the login page resets and does not redirect the user back to the JavaScript client. The only change is the below code and a custom implementation of IProfileService.
Below is the custom code in the login action (showing only the relevant part):
var apiClient = _httpClientFactory.CreateClient("API");
var request = new HttpRequestMessage(HttpMethod.Post, "/api/auth");
var loginModel = new LoginModel
{
Email = model.Email,
Password = model.Password
};
var content = new StringContent(JsonConvert.SerializeObject(loginModel),
Encoding.UTF8, "application/json");
request.Content = content;
HttpResponseMessage result = await apiClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
var loginStatus = JsonConvert.DeserializeObject<ApiLoginStatus>(
await result.Content.ReadAsStringAsync());
if (loginStatus.LoginSuccess)
{
await _events.RaiseAsync(new UserLoginSuccessEvent(model.Email, model.Email, loginStatus.Name, clientId: context?.ClientId));
AuthenticationProperties props = null;
if (AccountOptions.AllowRememberLogin && model.RememberLogin)
{
props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
};
};
var user = new IdentityServerUser(loginStatus.SubjectId)
{
DisplayName = loginStatus.Name
};
await HttpContext.SignInAsync(user, props);
if (context != null)
{
if (await _clientStore.IsPkceClientAsync(context.ClientId))
{
return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });
}
return Redirect(model.ReturnUrl);
}
The code actually hits the return View() path, but for some reason it resets and the login page is shown again.
Code in Startup.cs:
var builder = services.AddIdentityServer()
.AddInMemoryIdentityResources(Config.Ids)
.AddInMemoryApiResources(Config.Apis)
.AddInMemoryClients(Config.Clients)
.AddProfileService<ProfileService>()
.AddDeveloperSigningCredential();
Code in ProfileService.cs:
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var profile = await GetUserProfile(context.Subject.GetSubjectId());
var claims = new List<Claim>
{
new Claim(ClaimTypes.Email, profile.Email),
new Claim(ClaimTypes.Name, profile.Name)
};
context.IssuedClaims.AddRange(claims);
}
public async Task IsActiveAsync(IsActiveContext context)
{
var profile = await GetUserProfile(context.Subject.GetSubjectId());
context.IsActive = (profile != null);
}
There are multiple sources online that show how to user a custom store for authentication, but they all seem to use ResourceOwnerPasswordValidator. If someone could point out what is missing here, it would help greatly. Thanks.
So the issue turned out to be very simple. We had missed removing the builder.AddTestUsers(TestUsers.Users) line when setting up IdentityServer in Startup.cs.
Looking at the code here, it turned out that this line was overriding our profile service with the test users profile service. Removing that line solved the problem.
I am trying to figure out how to implement a delegation grant type in conjunction with client credentials, by following the tutorial from HERE, which is literally one page, since I have and API1 resource calling another API2 resource.
I've implemented the IExtensionGrantValidator and copied the code from the docs using the class name they provided, and added the client with grant type delegation. However, I am not sure where and how to call this method below, at first I was calling it from the client and tried passing the JWT I initially got to call API1 to the DelegateAsync method but I kept getting a bad request
In API 1 you can now construct the HTTP payload yourself, or use the IdentityModel helper library:
public async Task<TokenResponse> DelegateAsync(string userToken)
{
var payload = new
{
token = userToken
};
// create token client
var client = new TokenClient(disco.TokenEndpoint, "api1.client", "secret");
// send custom grant to token endpoint, return response
return await client.RequestCustomGrantAsync("delegation", "api2", payload);
}
So, I tried from API1 requesting a token in a method called GetAPI2Response which attempts to call a method in API2:
[HttpGet]
[Route("getapi2response")]
public async Task<string> GetApi2Response()
{
var client = new HttpClient();
var tokenResponse = await client.RequestTokenAsync(new TokenRequest
{
Address = "http://localhost:5005/connect/token",
GrantType = "delegation",
ClientId = "api1_client",
ClientSecret = "74c4148e-70f4-4fd9-b444-03002b177937",
Parameters = { { "scope", "stateapi" } }
});
var apiClient = new HttpClient();
apiClient.SetBearerToken(tokenResponse.AccessToken);
var response = await apiClient.GetAsync("http://localhost:6050/api/values");
if (!response.IsSuccessStatusCode)
{
Debug.WriteLine(response.StatusCode);
}
else
{
var content = await response.Content.ReadAsStringAsync();
return content;
}
return "failed";
}
However, this returns when debugging an invalid grant type. Strangely, I noticed when running IDSRV the code in the IExtensionGrantValidator method does not get hit, until you click the link for the discovery docs then it appears as a grant type
I'm obviously doing something wrong since I am not including the aforementioned DelegateAsync method from the docs, as its not clear to me where it goes.
The docs seem to be a bit outdated. With the actual extension methods there must be something like:
var tokenResponse = await client.RequestTokenAsync(new TokenRequest
{
Address = "http://localhost:5005/connect/token",
GrantType = "delegation",
ClientId = "api1_client",
ClientSecret = "74c4148e-70f4-4fd9-b444-03002b177937",
Parameters = new Dictionary<string, string>{{ "token", userToken }, { "scope", "stateapi" } }
})
you already implemented it, but forgot to add the initial token. When you extract it from the GetApi2Response() it can become your DelegateAsync.
Then your client configuration in Identityserver has to contain the delegation GrantType for the api1_client. Also don't forget the registration:
services.AddIdentityServer().AddExtensionGrantValidator<YourIExtensionGrantValidatorImpl>()
I'm working on a Xamarin Forms mobile app with .NET backend. I followed this guide and successfully set up custom authentications with one change in Startup.cs:
app.UseAppServiceAuthentication(new AppServiceAuthenticationOptions
{
SigningKey = Environment.GetEnvironmentVariable("WEBSITE_AUTH_SIGNING_KEY"),
ValidAudiences = new[] { Identifiers.Environment.ApiUrl },
ValidIssuers = new[] { Identifiers.Environment.ApiUrl },
TokenHandler = config.GetAppServiceTokenHandler()
});
Without "if (string.IsNullOrEmpty(settings.HostName))". Otherwise I am always getting unauthorized for all requests after login.
Server project:
Auth controller
public class ClubrAuthController : ApiController
{
private readonly ClubrContext dbContext;
private readonly ILoggerService loggerService;
public ClubrAuthController(ILoggerService loggerService)
{
this.loggerService = loggerService;
dbContext = new ClubrContext();
}
public async Task<IHttpActionResult> Post(LoginRequest loginRequest)
{
var user = await dbContext.Users.FirstOrDefaultAsync(x => x.Email == loginRequest.username);
if (user == null)
{
user = await CreateUser(loginRequest);
}
var token = GetAuthenticationTokenForUser(user.Email);
return Ok(new
{
authenticationToken = token.RawData,
user = new { userId = loginRequest.username }
});
}
private JwtSecurityToken GetAuthenticationTokenForUser(string userEmail)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, userEmail)
};
var secretKey = Environment.GetEnvironmentVariable("WEBSITE_AUTH_SIGNING_KEY");
var audience = Identifiers.Environment.ApiUrl;
var issuer = Identifiers.Environment.ApiUrl;
var token = AppServiceLoginHandler.CreateToken(
claims,
secretKey,
audience,
issuer,
TimeSpan.FromHours(24)
);
return token;
}
}
Startup.cs
ConfigureMobileAppAuth(app, config, container);
app.UseWebApi(config);
}
private void ConfigureMobileAppAuth(IAppBuilder app, HttpConfiguration config, IContainer container)
{
config.Routes.MapHttpRoute("ClubrAuth", ".auth/login/ClubrAuth", new { controller = "ClubrAuth" });
app.UseAppServiceAuthentication(new AppServiceAuthenticationOptions
{
SigningKey = Environment.GetEnvironmentVariable("WEBSITE_AUTH_SIGNING_KEY"),
ValidAudiences = new[] { Identifiers.Environment.ApiUrl },
ValidIssuers = new[] { Identifiers.Environment.ApiUrl },
TokenHandler = config.GetAppServiceTokenHandler()
});
}
Client project:
MobileServiceUser user = await MobileClient.LoginAsync(loginProvider, jtoken);
Additionally I configured Facebook provider in azure portal like described here.
But it works only when I comment out app.UseAppServiceAuthentication(new AppServiceAuthenticationOptions(){...}); in Startup.cs.
What I am missing to make both types of authentication works at the same time?
Since you have App Service Authentication/Authorization enabled, that will already validate the token. It assumes things about your token structure, such as having the audience and issuer be the same as your app URL (as a default).
app.UseAppServiceAuthentication() will also validate the token, as it is meant for local development. So in your example, the token will be validated twice. Aside from the potential performance impact, this is generally fine. However, that means the tokens must pass validation on both layers, and I suspect that this is not the case, hence the error.
One way to check this is to inspect the tokens themselves. Set a breakpoint in your client app and grab the token you get from LoginAsync(), which will be part of that user object. Then head to a service like http://jwt.io to see what the token contents look like. I suspect that the Facebook token will have a different aud and iss claim than the Identifiers.Environment.ApiUrl you are configuring for app.UseAppServiceAuthentication(), while the custom token probably would match it since you're using that value in your first code snippet.
If that holds true, than you should be in a state where both tokens are failing. The Facebook token would pass the hosted validation but fail on the local middleware, while the custom token would fail the hosted validation but pass the local middleware.
The simplest solution here is to remove app.UseAppServiceAuthentication() when hosting in the cloud. You will also need to make sure that your call to CreateToken() uses the cloud-based URL as the audience and issuer.
For other folks that find this issue
The documentation for custom authentication can be found here.
A general overview of App Service Authentication / Authorization can be found here.
The code you reference is only for local deployments. For Azure deployments, you need to turn on App Service Authentication / Authorization - even if you don't configure an auth provider (which you wouldn't in the case of custom auth).
Check out Chapter 2 of my book - http://aka.ms/zumobook