I can find a guide in "Learn by Example" how to Secure a Service with OAuth2 [1]. This example uses a separate introspection server as below.
oauth2:InboundOAuth2Provider oauth2Provider = new ({
url: "https://localhost:9095/oauth2/token/introspect"
});
So is there any guide/article I can use to follow to implement an introspection server, so that I can write a complete OAuth2 scenario to secure my ballerina service with OAuth2?
[1] https://ballerina.io/v1-2/learn/by-example/secured-service-with-oauth2.html
You can implement your own OAuth2 introspection server according to the instructions given by the RFC https://www.rfc-editor.org/rfc/rfc7662.
A draft implementation can be found below. You have to extract and validate the received token agains the issued access tokens by the server.
import ballerina/config;
import ballerina/http;
listener http:Listener oauth2Server = new(9095, {
secureSocket: {
keyStore: {
path: config:getAsString("keystore"),
password: config:getAsString("keystorePassword")
}
}
});
service oauth2 on oauth2Server {
#http:ResourceConfig {
methods: ["POST"],
path: "/token/introspect"
}
// This introspect the access token against the access token store,
// which holds the issued access tokens.
resource function introspect(http:Caller caller, http:Request req) {
http:Response res = new;
var authorizationHeader = trap req.getHeader("Authorization");
if (authorizationHeader is string) {
// Validate the received authorization header and
// prepare the introspection response.
// (Refer: https://www.rfc-editor.org/rfc/rfc7662#section-2.2)
res = ...;
} else {
// Invalid client.
// (Refer: https://www.rfc-editor.org/rfc/rfc6749#section-5.2)
res.statusCode = 401;
res.setPayload("invalid_client");
}
checkpanic caller->respond(res);
}
}
Related
I've developing an Angular web application using ASP.Net Core 3.1 for the API.
So far, I've written some integration unit tests using a Custom WebApplicationFactory to create the test server.
All tests use the HttpClient to make GETs and POSTs to the API running under the Custom WebApplicationFactory. Most of these tests initially perform a login to obtain a token to use for subsequent requests.
I'd like to add Two Factor Authentication to the application, but this will inevitably break any tests, as they aren't able to get hold of the six digit code which would be sent via email.
Here is what a test currently looks like, without MFA being implemented.
Is there a way that the test can be given the MFA code so that it can continue to perform tests?
Do I simply need to seed a user that does not have MFA enabled?
I actually want all users to have MFA enabled in production.
Many thanks
using Xunit;
using System.Threading.Tasks;
using MyCompany.ViewModels.Authentication;
using MyCompany.StaffPortal.Tests.Shared;
using StaffPortal;
using Newtonsoft.Json;
using MyCompany.ServiceA.ViewModels;
using System.Collections.Generic;
using System.Net.Http;
namespace MyCompany.Tests.StaffPortal.ServiceA
{
public class ExtensionsControllerTests : TestBase
{
public ExtensionsControllerTests(CustomWebApplicationFactory<Startup> factory) : base(factory)
{
}
[Fact]
public async Task Test_GetExtensions()
{
//This line creates a new "web browser" and uses the login details provided to obtain and set up the token so that we can request information about an account.
HttpClient httpClient = await CreateAuthenticatedHttpClient("abcltd1#MyCompany.com", "test", 1);
//Perform any work and get the information from the API
//Contact the API using the token so check that it works
var getExtensionsResponse = await httpClient.GetAsync("/api/ServiceA/extensions/GetExtensions");
//Check that the response was OK
Assert.True(getExtensionsResponse.StatusCode == System.Net.HttpStatusCode.OK, "GetExtensions did not return an OK result.");
//Get and Convert the Content we received into a List of ServiceAExtensionViewModel, as that is what GetExtensions sends back to the browser.
var getExtensionsResponseContent = await getExtensionsResponse.Content.ReadAsStringAsync();
List<ServiceAExtensionViewModel> extensionList = JsonConvert.DeserializeObject<List<ServiceAExtensionViewModel>>(getExtensionsResponseContent);
//Check the information received matches our expectations
Assert.True(extensionList.Count == 2);
Assert.True(extensionList[0].PropertyA == 123);
Assert.True(extensionList[0].PropertyB == 0161);
Assert.True(extensionList[0].PropertyC == true);
}
}
}
Here is the content's of CreateAuthenticatedHttpClient() for reference.
protected async Task<HttpClient> CreateAuthenticatedHttpClient(string username, string password, int companyAccountId)
{
var httpClient = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
//Create the Login information to send to the server
var loginInformation = new LoginRequestModel
{
Username = username,
Password = password,
ReturnUrl = ""
};
//Convert it into Json which the server will understand
var validLoginRequestJson = ConvertToJson(loginInformation);
//Send the Json Login information to the server, and put the response we receive into loginResponse
//In the code below, httpClient is like a web browser. You give it the
var loginResponse = await httpClient.PostAsync("/api/authenticate", validLoginRequestJson);
//Check the loginResponse was a CREATED response, which means that the token was made
Assert.True(loginResponse.StatusCode == System.Net.HttpStatusCode.Created, "New Token was not returned.");
//Check the response is identified as being in Json format
Assert.Equal("application/json; charset=utf-8", loginResponse.Content.Headers.ContentType.ToString());
//Next we have to convert the received Json information into whatever we are expecting.
//In this case, we are expecting a AuthenticationResponseViewModel (because that's what the API sends back to the person trying to log in)
//First we get hold of the Content (which is in Json format)
var responseJsonString = await loginResponse.Content.ReadAsStringAsync();
//Second we convert the Json back into a real AuthenticationResponseViewModel
AuthenticationResponseViewModel authenticationResponseViewModel = JsonConvert.DeserializeObject<AuthenticationResponseViewModel>(responseJsonString);
//Now we take the Token from AuthenticationResponseViewModel, and add it into the httpClient so that we can check the Token works.
httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", authenticationResponseViewModel.token);
httpClient.DefaultRequestHeaders.Add("CompanyId", companyAccountId.ToString());
return httpClient;
}
Working on a project which integrates Google Cloud's speech-to-text api in an android and iOS environment. Ran through the example code provided (https://cloud.google.com/speech-to-text/docs/samples) and was able to get it to run. Used them as a template to add voice into my app, however there is a serious danger in the samples, specifically in generating the AccessToken (Android snippet below):
// ***** WARNING *****
// In this sample, we load the credential from a JSON file stored in a raw resource
// folder of this client app. You should never do this in your app. Instead, store
// the file in your server and obtain an access token from there.
// *******************
final InputStream stream = getResources().openRawResource(R.raw.credential);
try {
final GoogleCredentials credentials = GoogleCredentials.fromStream(stream)
.createScoped(SCOPE);
final AccessToken token = credentials.refreshAccessToken();
This was fine to develop and test locally, but as the comment indicates, it isn't safe to save the credential file into a production app build. So what I need to do is replace this code with a request from a server endpoint. Additionally i need to write the endpoint that will take the request and pass back a token. Although I found some very interesting tutorials related to Firebase Admin libraries generating tokens, I couldn't find anything related to doing a similar operation for GCP apis.
Any suggestions/documentation/examples that could point me in the right direction are appreciated!
Note: The server endpoint will be a Node.js environment.
Sorry for the delay, I was able to get it all to work together and am now only circling back to post an extremely simplified how-to. To start, I installed the following library on the server endpoint project https://www.npmjs.com/package/google-auth-library
The server endpoint in this case is lacking any authentication/authorization etc for simplicity's sake. I'll leave that part up to you. We are also going to pretend this endpoint is reachable from https://www.example.com/token
The expectation being, calling https://www.example.com/token will result in a response with a string token, a number for expires, and some extra info about how the token was generated:
ie:
{"token":"sometoken", "expires":1234567, "info": {... additional stuff}}
Also for this example I used a ServiceAccountKey file which will be stored on the server,
The suggested route is to set up a server environment variable and use https://cloud.google.com/docs/authentication/production#finding_credentials_automatically however this is for the examples sake, and is easy enough for a quick test. These files look something like the following: ( honor system don't steal my private key )
ServiceAccountKey.json
{
"type": "service_account",
"project_id": "project-id",
"private_key_id": "378329234klnfgdjknfdgh9fgd98fgduiph",
"private_key": "-----BEGIN PRIVATE KEY-----\nThisIsTotallyARealPrivateKeyPleaseDontStealIt=\n-----END PRIVATE KEY-----\n",
"client_email": "project-id#appspot.gserviceaccount.com",
"client_id": "12345678901234567890",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/project-id%40appspot.gserviceaccount.com"
}
So here it is a simple endpoint that spits out an AccessToken and a number indicating when the token expires (so you can call for a new one later).
endpoint.js
const express = require("express");
const auth = require("google-auth-library");
const serviceAccount = require("./ServiceAccountKey.json");
const googleauthoptions = {
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
credentials: serviceAccount
};
const app = express();
const port = 3000;
const auth = new auth.GoogleAuth(googleauthoptions);
auth.getClient().then(client => {
app.get('/token', (req, res) => {
client
.getAccessToken()
.then((clientresponse) => {
if (clientresponse.token) {
return clientresponse.token;
}
return Promise.reject('unable to generate an access token.');
})
.then((token) => {
return client.getTokenInfo(token).then(info => {
const expires = info.expiry_date;
return res.status(200).send({ token, expires, info });
});
})
.catch((reason) => {
console.log('error: ' + reason);
res.status(500).send({ error: reason });
});
});
app.listen(port, () => {
console.log(`Server is listening on https://www.example.com:${port}`);
});
return;
});
Almost done now, will use android as an example. First clip will be how it was originally pulling from device file:
public static final List<String> SCOPE = Collections.singletonList("https://www.googleapis.com/auth/cloud-platform");
final GoogleCredentials credentials = GoogleCredentials.fromStream(this.mContext.getResources().openRawResource(R.raw.credential)).createScoped(SCOPE);
final AccessToken token = credentials.refreshAccessToken();
final string token = accesstoken.getTokenValue();
final long expires = accesstoken.getExpirationTime().getTime()
final SharedPreferences prefs = getSharedPreferences(PREFS, Context.MODE_PRIVATE);
prefs.edit().putString(PREF_ACCESS_TOKEN_VALUE, value).putLong(PREF_ACCESS_TOKEN_EXPIRATION_TIME, expires).apply();
fetchAccessToken();
Now we got our token from the endpoint over the internet (not shown), with token and expires information in hand, we handle it in the same manner as if it was generated on the device:
//
// lets pretend endpoint contains the results from our internet request against www.example.com/token
final string token = endpoint.token;
final long expires = endpoint.expires
final SharedPreferences prefs = getSharedPreferences(PREFS, Context.MODE_PRIVATE);
prefs.edit().putString(PREF_ACCESS_TOKEN_VALUE, value).putLong(PREF_ACCESS_TOKEN_EXPIRATION_TIME, expires).apply();
fetchAccessToken();
Anyway hopefully that is helpful if anyone has a similar need.
===== re: AlwaysLearning comment section =====
Compared to the original file credential based solution:
https://github.com/GoogleCloudPlatform/android-docs-samples/blob/master/speech/Speech/app/src/main/java/com/google/cloud/android/speech/SpeechService.java
In my specific case I am interacting with a secured api endpoint that is unrelated to google via the react-native environment ( which sits on-top of android and uses javascript ).
I already have a mechanism to securely communicate with the api endpoint I created.
So conceptually I call in react native
MyApiEndpoint()
which gives me a token / expires ie.
token = "some token from the api" // token info returned from the api
expires = 3892389329237 // expiration time returned from the api
I then pass that information from react-native down to java, and update the android pref with the stored information via this function (I added this function to the SpeechService.java file)
public void setToken(String value, long expires) {
final SharedPreferences prefs = getSharedPreferences(PREFS, Context.MODE_PRIVATE);
prefs.edit().putString(PREF_ACCESS_TOKEN_VALUE, value).putLong(PREF_ACCESS_TOKEN_EXPIRATION_TIME, expires).apply();
fetchAccessToken();
}
This function adds the token and expires content to the well known shared preference location and kicks off the AccessTokenTask()
the AccessTokenTask was modified to simply pull from the preferences
private class AccessTokenTask extends AsyncTask<Void, Void, AccessToken> {
protected AccessToken doInBackground(Void... voids) {
final SharedPreferences prefs = getSharedPreferences(PREFS, Context.MODE_PRIVATE);
String tokenValue = prefs.getString(PREF_ACCESS_TOKEN_VALUE, null);
long expirationTime = prefs.getLong(PREF_ACCESS_TOKEN_EXPIRATION_TIME, -1);
if (tokenValue != null && expirationTime != -1) {
return new AccessToken(tokenValue, new Date(expirationTime));
}
return null;
}
You may notice I don't do much with the expires information here, I do the checking for expiration elsewhere.
Here you have a couple of useful links:
Importing the Google Cloud Storage Client library in Node.js
Cloud Storage authentication
I've set up an API Gateway using WebSocket protocol. On the '$connect' route request setting, I selected 'AWS_IAM' as the authorization method. The web app needs to make a connection to this WebSocket API after a user logged in via Cognito. How do I then authorize the WebSocket API request from the JavaScript on the web app? With the HTTP API Gateway, I can generate the signature from access key and session token, which got passed in to the request header. But I can't pass headers in a WebSocket request.
This is some example/pseudo code that works for me:
Using AWS Amplify Authenticated user:
import { w3cwebsocket as W3CWebSocket } from "websocket"
import { Auth, Signer } from "aws-amplify"
let wsClient: any = null
export const client = async () => {
if (wsClient) return wsClient
if ((await Auth.currentUserInfo()) === null) return wsClient
const credentials = await Auth.currentCredentials()
const accessInfo = {
access_key: credentials.accessKeyId,
secret_key: credentials.secretAccessKey,
session_token: credentials.sessionToken,
}
const wssUrl = "wss://YOUR-API-ID.execute-api.REGION.amazonaws.com/dev"
const signedUrl = Signer.signUrl(wssUrl, accessInfo)
wsClient = new W3CWebSocket(signedUrl)
wsClient.onerror = function () {
console.log("[client]: Connection Error")
}
wsClient.onopen = function () {
console.log("[client]: WebSocket Client Connected")
}
wsClient.onclose = function () {
console.log("[client]: Client Closed")
}
wsClient.onmessage = function (e: any) {
if (typeof e.data === "string") {
console.log("Received: '" + e.data + "'")
}
}
return wsClient
}
Then also using AWS Cognito needs this permission:
{
"Action": ["execute-api:Invoke"],
"Resource": "arn:aws:execute-api:REGION:ACCOUNT-ID-OR-WILDCARD:*/*/$connect",
"Effect": "Allow"
}
I have got an answer from AWS support. I will need to sign the wss URL. So instead of setting request headers in a HTTP request, the signature information will be passed to the url in the query string parameters. A signed wss URL looks like: wss://API_ID.execute-api.region.amazonaws.com/dev?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ACCESSKEY/20200131/region/execute-api/aws4_request&X-Amz-Date=20200131T100233Z&X-Amz-Security-Token=SECURITY_TOKEN&X-Amz-SignedHeaders=host&X-Amz-Signature=SIGNATURE.
To generate the signed URL, I can use Signer.signUrl method from #aws-amplify/core library.
I implemented this Dart code which signs the AWS request URLs. This is particularly helpful to connect to a IAM-secured WebSocket API Gateway.
https://github.com/MohammedNoureldin/aws-url-signer
I know that putting links in answers in discouraged, but this will not make sense to copy the whole 100 lines of code here.
The usage of my implementation will look like this:
String getSignedWebSocketUrl(
{String apiId,
String region,
String stage,
String accessKey,
String secretKey,
String sessionToken})
My app first uses the Cognito LOGIN endpoint to obtain an Authorization Code. It then uses the TOKEN endpoint to try and obtain tokens (id_token, access_token, refresh_token) but that fails with unauthorized_client.
I do not understand why, the same client is used to access the LOGIN, and that succeeded in returning an authorization code. I'm following the documentation for the TOKEN endpoint
string clientId = ...
string clientSecret = ...
Uri redirectUri = new Uri("myapp://myhost");
string authorization_code = ... // obtained via HTTP GET on LOGIN endpoint
string accessTokenUrl = "https://<domain>.auth.<region>.amazoncognito.com/oauth2/token";
var queryValues = new Dictionary<string, string>
{
{ "grant_type", "authorization_code" },
{ "code", authorization_code },
{ "redirect_uri", redirectUri.AbsoluteUri },
{ "client_id", clientId},
};
using (HttpClient client = new HttpClient())
{
// Authorization Basic header with Base64Encoded (clientId::clientSecret)
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Basic",
Convert.ToBase64String(System.Text.ASCIIEncoding.ASCII.GetBytes(
string.Format("{0}:{1}",
clientId,
clientSecret))));
// Url Encoded Content
var content = new FormUrlEncodedContent(queryValues);
// HTTPS POST
HttpResponseMessage response = await client.PostAsync(accessTokenUrl, content).ConfigureAwait(false);
string text = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
// test = {"error" : "unauthorized_client"}
}
The problem is two-fold:
1- System.Uri.AbsoluteUri adds a trailing / in the returned string so that my redirectUri becomes myapp://myhost/ instead of myapp://myhost
2- AWS Cognito TOKEN endpoint does not accept trailing / in a redirectURI.
The solution:
I now call redirectUri.OriginalUri instead of redirectUri.AbsoluteUri where I build the query to preserve the redirectUri as it was when I built it.
(I don't really have control over this since in my case Xamarin.Auth.OAuthAuthenticator2 calls Uri.AbsoluteUri on my behalf and transforms the redirectUri string I gave it, so I'm going to have to fix Xamarin.Auth).
I use token auth for my WebApi application.
I have the following ConfigureAuth method in Startup class:
// Configure the application for OAuth based flow
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
// In production mode set AllowInsecureHttp = false
AllowInsecureHttp = true
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
and ApplicationOAuthProvider:
public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider
{
private readonly string _publicClientId;
public ApplicationOAuthProvider(string publicClientId)
{
if (publicClientId == null)
{
throw new ArgumentNullException("publicClientId");
}
_publicClientId = publicClientId;
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
var user = await userManager.FindAsync(context.UserName, context.Password);
//ApplicationUser user = new ApplicationUser() { UserName ="a" };
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = CreateProperties(user.UserName);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}
so, I should call /Token and pass credentials to get token. It works, but I want to create Unit Test for it. Is it possible?
The only way to do that is by make an integration test, which asserts the full pipeline testing - from request to response. Before the actual test on the server, you can call the token endpoint to get it, and then use it in the actual unit test by attaching it to the response. I have a sample, which uses MyTested.WebApi here:
Sample
You can do the same without the testing library, this is just how to do it.
I like the idea of pluggable configuration.
For Unit Test project, I want to use specific identity and get predictable data fro LDAP. So, i use the following line in my unit test method when setting http configuration:
config.Filters.Add(new WebApiSetIdentityFilter(config, identityName));
where the filter just "hacks" the identity, replacing the fields I need:
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
//This principal flows throughout the request.
context.Principal = new GenericPrincipal(new GenericIdentity(this.IdentityName, "LdapAuthentication"), new string[0]);
}