I am trying to hide my whole application behind a authentication (I'll deal with authorization when this works), and for now I want to every url to require github login. I am not getting the github login page up.
I have tried to combine the SAFE-stack template and "Using OAuth with Saturn", but I do not get the github login page (which I do get when following only the Saturn guide), I just get the normal todo page. If I click the Add button, the server prints
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/1.1 POST http://localhost:8085/api/ITodosApi/addTodo application/json; charset=UTF-8 68
info: Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler`1[[Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions, Microsoft.AspNetCore.Authentication.OAuth, Version=3.1.11.0, Culture=neutral, PublicKeyToken=adb9793829ddae60]][12]
AuthenticationScheme: GitHub was challenged.
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
Request finished in 10.8057ms 302
And the item is not added to the list of todos.
I have tried shaving off everything except the authentication bit, and compared to a clean safe template, the only changes I have made is
dotnet paket add Saturn.Extensions.Authorization --project src/Server/Server.fsproj
Manually downgrade two dependencies in paket.lock that otherwise created an error: Microsoft.AspNetCore.Authentication.Google to (3.1.11) and Microsoft.AspNetCore.Authentication.OpenIdConnect to (3.1.11)
Change the app value in Server/Server.fs to the following (I created a new github auth app for this issue):
let loggedInPipeline = pipeline {
requires_authentication (Giraffe.Auth.challenge "GitHub")
}
let loggedInView = router {
pipe_through loggedInPipeline
get "/" webApp
}
let appRouter = router {
forward "" loggedInView
}
let app =
application {
use_router appRouter
url "http://0.0.0.0:8085/"
memory_cache
use_static "public"
use_gzip
use_github_oauth "8cde657dfd1d3a41b9ed" "0b245e12900ff8486ade076aae07aa0deb0fd83d" "/signin-github" [("login", "githubUsername"); ("name", "fullName")]
}
run app
My gitHub app config auth callback url: http://localhost:8080/signin-github
I got help on solving this, just for AzureAD. A blog post can be found here https://www.compositional-it.com/news-blog/safe-stack-authentication-with-active-directory-part-2/. Should be the same for github. What I needed to do was to make some changes to webpack.config.js, Server.fs and build.fsx
webpack.config.js
devServerProxy: {
// redirect all requests to the server on port 8085
'**': {
//...
var CONFIG = {
appHtmlTemplate: './src/Client/app.html',
//...
var commonPlugins = [
new HtmlWebpackPlugin({
filename: 'app.html',
//...
var CONFIG = {
// ... other webpack config settings
outputDir: './src/Server/public',
// ...
devServer: {
// ...other dev server config settings
writeToDisk: true
},
Server.fs:
let authScheme = "AzureAD"
let isDevelopment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") = Environments.Development;
let noAuthenticationRequired nxt ctx = task { return! nxt ctx }
let authChallenge : HttpFunc -> HttpContext -> HttpFuncResult =
requiresAuthentication (Auth.challenge authScheme)
let routes =
choose [
route "/" >=> authChallenge >=> htmlFile "public/app.html"
]
build.fsx:
let serverPublicPath = Path.getFullName "./src/Server/public"
let clientPublicPath = Path.getFullName "./src/Client/public"
Target.create "Clean" (fun _ ->
Shell.cleanDir deployDir
Shell.cleanDir serverPublicPath)
Target.create "Run" (fun _ ->
Shell.copyDir serverPublicPath clientPublicPath FileFilter.allFiles
//... other commands
You will need to use a non-Chrome browser if you are working locally, such as Firefox, due to the cookie issue mentioned earlier.
It is a good idea to open a private browsing window, to make sure that you don't already have a logged in account etc.
If you are having issues, check that you have
properly set up your app's Active Directory registration in Azure
added the required AD configuration to your Server in appsettings.json, including the login / logout callback urls you set in the AD registration.
have you tried https instead of http for your url parameter?
Replace
url "http://0.0.0.0:8085"
with
url "https://0.0.0.0:8085"
This fixed the problem for me.
Demo code: https://github.com/functionalfriday/fsharp-saturn-demos
Related
I'm integrating Azure AD and MS-Identity on a web app with Angular.
It works on my machine, but when I deploy it, I get an issue with the callback URL.
First, to make sure the callback URL is ok, I extract it from the microsoft login popup window's URL:
Then, I url decode the content. The URL seems fine and it is available in my Azure app's redirect URL.
Then I login to Microsoft normally and I get this error (AADSTS50011):
Then I inspect the URL again (inside the query string from the urldecoded popup window's URL) and now the URL seems to have been "tampered with".
It's now something like this:
http://somedomain:80/some_page/somequerystring
instead of
https://somedomain/some_page/somequerystring
so I wonder if it's part of the problem or if it's normal behavior.
It is also mentionned "If you contact your administrator, send this info to them." I suppose I'm the "administrator" so what can I do with that "Copy info to clipboard" info to investigate the problem?
Is your application hosting on http (80) or https (443)? If your app service is terminating your TLS connection and handling that for you instead of your app, your sign-on will construct the redirect using the http request scheme. I hooked into the OnRedirectToIdentityProvider event to correct the scheme.
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
Configuration.Bind("AzureAd", options);
options.Events ??= new OpenIdConnectEvents();
options.Events.OnRedirectToIdentityProvider += _fixRedirect;
});
...
private async Task _fixRedirect(RedirectContext context)
{
context.Request.Scheme = "https";
if(!context.ProtocolMessage.RedirectUri.StartsWith("https"))
context.ProtocolMessage.RedirectUri =
context.ProtocolMessage.RedirectUri.Replace("http", "https");
await Task.CompletedTask;
}
I am trying to create websocket server and client in my iOS app, which i successfully managed to do with the help of sample implementation here. (https://github.com/apple/swift-nio/tree/master/Sources/NIOWebSocketServer) - so current working situation is, i run the websocket server when app launches and then I load the client in a webview which can connect to it.
Now my problem is I want my server to secured websocket server (Basically connect to the websocket server from a HTTPS html page)
I am new to network programming and Swift-nio documentation is lacking to say the least. As far as I understand I could use (https://github.com/apple/swift-nio-transport-services)
I found this thread which is exactly what I need - https://github.com/apple/swift-nio-transport-services/issues/39 - I could disable the TLS authentication as I dont care in my usecase as long as I could get the websocket connected.
So my question is how to I extend my client (https://github.com/apple/swift-nio/tree/master/Sources/NIOWebSocketClient) and server (https://github.com/apple/swift-nio/tree/master/Sources/NIOWebSocketServer) to use swift-nio-transport-service.
I could add the NIOSSLContext and stuff but I think I need to add the EventLoopGroup and new bootstrap methods. I know the answers is right there.... but I just cannot seem to pinpoint it.
Any pointer would be appreciated.
Thanks.
To translate a simple NIO Server to a NIOTransportServices one, you need to make the following changes:
Add a dependency on NIOTransportServices to your server.
Change MultiThreadedEventLoopGroup to NIOTSEventLoopGroup.
Change ClientBootstrap to NIOTSConnectionBootstrap.
Change ServerBootstrap to NIOTSListenerBootstrap.
Build and run your code.
Some ChannelOptions don’t work in NIOTransportServices, but most do: the easiest way to confirm that things are behaving properly is to quickly test the common flow.
This doesn’t add any extra functionality to your application, but it does give you the same functionality using the iOS APIs.
To add TLS to either NIOTSConnectionBootstrap or NIOTSListenerBootstrap, you use the .tlsOptions function. For example:
NIOTSListenerBootstrap(group: group)
.tlsOptions(myTLSOptions())
Configuring a NWProtocolTLS.Options is a somewhat tricky thing to do. You need to obtain a SecIdentity, which requires interacting with the keychain. Quinn has discussed this somewhat here.
Once you have a SecIdentity, you can use it like so:
func myTLSOptions() -> NWProtocolTLS.Options {
let options = NWProtocolTLS.Options()
let yourSecIdentity = // you have to implement something here
sec_protocol_options_set_local_identity(options.securityProtocolOptions, sec_identity_create(yourSecIdentity)
return options
}
Once you have that code written, everything should go smoothly!
As an extension, if you wanted to secure a NIO server on Linux, you can do so using swift-nio-ssl. This has separate configuration as the keychain APIs are not available, and so you do a lot more loading of keys and certificates from files.
I needed a secure websocket without using SecIdentity or NIOTransportServices, so based on #Lukasa's hint about swift-nio-ssl I cobbled together an example that appears to work correctly.
I dunno if it's correct, but I'm putting it here in case someone else can benefit. Error-handling and aborting when the try's fail is left out for brevity.
let configuration = TLSConfiguration.forServer(certificateChain: try! NIOSSLCertificate.fromPEMFile("/path/to/your/tlsCert.pem").map { .certificate($0) }, privateKey: .file("/path/to/your/tlsKey.pem"))
let sslContext = try! NIOSSLContext(configuration: configuration)
let upgradePipelineHandler: (Channel, HTTPRequestHead) -> EventLoopFuture<Void> = { channel, req in
WebSocket.server(on: channel) { ws in
ws.send("You have connected to WebSocket")
ws.onText { ws, string in
print("Received text: \(string)")
}
ws.onBinary { ws, buffer in
// We don't accept any Binary data
}
ws.onClose.whenSuccess { value in
print("onClose")
}
}
}
self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 2)
let port: Int = 5759
let promise = self.eventLoopGroup!.next().makePromise(of: String.self)
_ = try? ServerBootstrap(group: self.eventLoopGroup!)
// Specify backlog and enable SO_REUSEADDR for the server itself
.serverChannelOption(ChannelOptions.backlog, value: 256)
.serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.childChannelInitializer { channel in
let handler = NIOSSLServerHandler(context: sslContext)
_ = channel.pipeline.addHandler(handler)
let webSocket = NIOWebSocketServerUpgrader(
shouldUpgrade: { channel, req in
return channel.eventLoop.makeSucceededFuture([:])
},
upgradePipelineHandler: upgradePipelineHandler
)
return channel.pipeline.configureHTTPServerPipeline(
withServerUpgrade: (
upgraders: [webSocket],
completionHandler: { ctx in
// complete
})
)
}.bind(host: "0.0.0.0", port: port).wait()
_ = try! promise.futureResult.wait()
try! server.close(mode: .all).wait()
I'm new to react and started with the create-react-app template.
I am developing a SPA, that is consuming a REST API.
For developing purposes, i need to authenticate against this API using OAUTH2 (Access Token).
In the production environment, i don't need to authenticate (as it runs on the same machine).
In order to get this access token in the dev environment, I need to make a POST request to the authentication server (with client_id and client_secret) and then I receive the access token which I need for further request.
As the authentication server does not support cors, I cannot do this post request within the react app.
My solution was to write a node script, that does this post request and inject the token to the client App using environment variables.
In the package.json (I did an eject) I inserted this script (gettoken):
"start": "npm-run-all -p watch-css gettoken-js start-js"
In the gettoken.js file I make the post request for getting the access token and set (in the callback function):
process.env.REACT_APP_CRM_API_ACCESS_TOKEN = response.access_token;
Now I want to access this variable in the react app - but here process.env.REACT_APP_CRM_API_ACCESS_TOKEN is always undefinied.
What am I doing wrong?
Is there another way to inject the access_token to the client app?
Here is the getToken script:
var request = require('request');
const endpoint = "https://xxxx/oauth2/token";
const resource = "https://xxxyyy.com";
const params = [
{
name: 'userName',
value: 'myuser#user.com'
},
{
name: 'password',
value: '123'
},
{
name: 'grant_type',
value: 'password'
},
{
name: 'client_secret',
value: '11231'
},
{
name: 'client_id',
value: '123'
},
{
name: 'resource',
value: resource
}
];
const encodedParams= Object.keys(params).map((key) => {
return params[key].name + '=' + encodeURIComponent(params[key].value);
}).join('&');
request(
{
method: 'POST',
uri: endpoint,
headers: [
{
name: 'content-type',
value: 'application/x-www-form-urlencoded'
}
],
body: encodedParams + "&"
}
,
function (error, response, body) {
//throw new Error("asdfasdf");
if (error)
{
console.log('error', "ERROR GETTING ACCESS TOKEN FROM API: " + error);
}
else
{
let response = JSON.parse(body);
process.env.REACT_APP_CRM_API_ACCESS_TOKEN = response.access_token;
process.env.REACT_APP_CRM_API_ENDPOINT = resource;
console.log(process.env.REACT_APP_CRM_API_ACCESS_TOKEN);
}
}
);
You're bumping into a common issue with configuration in a Create React App.
The REACT_APP_ prefix is used at build time. Are you building after you put REACT_APP_CRM_API_ACCESS_TOKEN into the env? If not, the app doesn't have them.
If you're happy to have the token in the JS bundle then go with that.
Depending on how you plan to promote your build through various environments, you'll likely bump into other issues.
Here's one possible pipeline and the issue you'll bump into.
You have staging and production.
You build your app and end up with staging env variables built into the bundle.
You promote that bundle to production and the staging env vars are still in the bundle.
Two ways around it:
Rebuild on production so the prod vars are put into the bundle.
Use the web server to inject the vars from the environment into one of the files in your SPA.
I've gone through this 4 times now and tweaked my solution slightly each time. Each time was on an ejected CRA app and each solution wasn't very DRY.
I'm now trying to solve it for a non ejected CRA and again, trying to find a DRY solution is proving tricky.
Will update this answer if I find a nicer way.
Edit: Because you have ejected the app, you can change config/env.js to do whatever you need. Including the way the REACT_APP_ prefix works.
I am using Identity server and hosting it under IIS. It was working fine when hosted directly under http://localhost:44431
Step 1: call http://localhost:44431/account/login?returnUrl=/connect/authorize/login?respone_type....
Step 2: Then it goes to the Authorize Endpoint and a return a token
Probelm hosting under localhost\id:
However, when I deploy the application on IIS under Default Web site as localhost\id. It stops working.
Step 1: Calling http://localhost/id/account/login?returnUrl=/connect/authorize/login?respone_type....
>> Inspecting the Request Headers:
>> Response Header:
>> Open Id Configuration at http://localhost/id/.well-known/openid-configuration
"authorization_endpoint":"http://localhost/id/connect/authorize",
Step 2: Calling the /connect/authorize endpoint:
>> Inspecting the Headers:
It didn't include the id virtual directory, that's why it is failing. where in the process I have to fix this?
I'm not able to reproduce your problem, but I did start from scratch hosting IdentityServer4 in IIS. The steps I followed for setup are below.
Cloned IdentityServer4.Samples. Launch Quickstarts/3_ImplicitFlowAuthentication solution:
https://github.com/IdentityServer/IdentityServer4.Samples/tree/release/Quickstarts/3_ImplicitFlowAuthentication
Created an application in IIS with the path as '/id' with the AppPool set to 'No Managed Code'
Ran 'dotnet publish' on the IdentityServer4 project and moved the output to the IIS app root's folder
Changed the Authority URL in the MvcClient project to point to localhost/id
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
AuthenticationScheme = "oidc",
SignInScheme = "Cookies",
Authority = "http://localhost/id",
RequireHttpsMetadata = false,
ClientId = "mvc",
SaveTokens = true
});
Load the MvcClient application and navigate to a route with the 'Authorize' filter. The redirect occurred properly with the appropriate virtual directory
Check to see if the proper path is being output by IdentityServer by going to the openid-configuration page: http://localhost/id/.well-known/openid-configuration
Are you running IdentityServer4 and an MVC app in the same project? If so, are you using relative paths for the OpenIdConnectOptions.Authority property? Try changing it to an absolute path and see if that fixes the problem. I'm thinking this might be the case, because your request URL does not include the /id path in the redirect uri:
http://localhost/id/account/login?**returnUrl=/connect/authorize/login**?respone_type
The correct path of course should be:
http://localhost/id/account/login?**returnUrl=/id/connect/authorize/login**?respone_type
Hope this helps! Please let me know
To use Events API for Slack App development, there is a setting for "Events API Request URLs" as described in doc:
In the Events API, your Events API Request URL is the target location
where all the events your application is subscribed to will be
delivered, regardless of the team or event type.
There is a UI for changing the URL "manually" at api.slack.com under
"Event Subscriptions" section in settings. There is also url_verification event after changing the Request URL described here.
My question - Is there an API call (method) so I can update the endpoint (Request URL) from my server code?
For example, in Facebook API there is a call named subscriptions where I can change webhook URL after initial setup - link
Making a POST request with the callback_url, verify_token, and object
fields will reactivate the subscription.
PS. To give a background, this is needed for development using outbound tunnel with dynamic endpoint URL, e.g. ngrok free subscription. By the way, ngrok is referenced in sample "onboarding" app by slack here
Update. I checked Microsoft Bot Framework, and they seems to use RTM (Real Time Messaging) for slack which doesn't require Request URL setup, and not Events API. Same time, e.g. for Facebook they (MS Bot) instruct me to manually put their generated URL to webhook settings of a FB app, so there is no automation on that.
Since this question was originally asked, Slack has introduced app manifests, which enable API calls to change app configurations. This can be used to update URLs and other parameters, or create/delete apps.
At the time of writing, the manifest / manifest API is in beta:
Beta API — this API is in beta, and is subject to change without the usual notice period for changes.
so the this answer might not exactly fit the latest syntax as they make changes.
A programatic workflow might look as follows:
Pull a 'template' manifest from an existing version of the application, with most of the settings as intended (scopes, name, etc.)
Change parts of the manifest to meet the needs of development
Verify the manifest
Update a slack app or create a new one for testing
API List
Basic API list
Export a manifest as JSON: apps.manifest.export
Validate a manifest JSON: apps.manifest.validate
Update an existing app: apps.manifest.update
Create a new app from manifest: apps.manifest.create
Delete an app: apps.manifest.delete
Most of these API requests are Tier 1 requests, so only on the order of 1+ per minute.
API Access
You'll need to create and maintain "App Configuration Tokens". They're created in the "Your Apps" dashboard. More info about them here.
Example NodeJS Code
const axios = require('axios');
// Change these values:
const TEMPLATE_APP_ID = 'ABC1234XYZ';
const PUBLIC_URL = 'https://www.example.com/my/endpoint';
let access = {
slackConfigToken: "xoxe.xoxp-1-MYTOKEN",
slackConfigRefreshToken: "xoxe-1-MYREFRESHTOKEN",
slackConfigTokenExp: 1648550283
};
// Helpers ------------------------------------------------------------------------------------------------------
// Get a new access token with the refresh token
async function refreshTokens() {
let response = await axios.get(`https://slack.com/api/tooling.tokens.rotate?refresh_token=${access.slackConfigRefreshToken}`);
if (response.data.ok === true) {
access.slackConfigToken = response.data.token;
access.slackConfigRefreshToken = response.data.refresh_token;
access.slackConfigTokenExp = response.data.exp;
console.log(access);
} else {
console.error('> [error] The token could not be refreshed. Visit https://api.slack.com/apps and generate tokens.');
process.exit(1);
}
}
// Get an app manifest from an existing slack app
async function getManifest(applicationID) {
const config = {headers: { Authorization: `Bearer ${access.slackConfigToken}` }};
let response = await axios.get(`https://slack.com/api/apps.manifest.export?app_id=${applicationID}`, config);
if (response.data.ok === true) return response.data.manifest;
else {
console.error('> [error] Invalid could not get manifest:', response.data.error);
process.exit(1);
}
}
// Create a slack application with the given manifest
async function createDevApp(manifest) {
const config = {headers: { Authorization: `Bearer ${access.slackConfigToken}` }};
let response = await axios.get(`https://slack.com/api/apps.manifest.create?manifest=${encodeURIComponent(JSON.stringify(manifest))}`, config);
if (response.data.ok === true) return response.data;
else {
console.error('> [error] Invalid could not create app:', response.data.error);
process.exit(1);
}
}
// Verify that a manifest is valid
async function verifyManifest(manifest) {
const config = {headers: { Authorization: `Bearer ${access.slackConfigToken}` }};
let response = await axios.get(`https://slack.com/api/apps.manifest.validate?manifest=${encodeURIComponent(JSON.stringify(manifest))}`, config);
if (response.data.ok !== true) {
console.error('> [error] Manifest did not verify:', response.data.error);
process.exit(1);
}
}
// Main ---------------------------------------------------------------------------------------------------------
async function main() {
// [1] Check token expiration time ------------
if (access.slackConfigTokenExp < Math.floor(new Date().getTime() / 1000))
// Token has expired. Refresh it.
await refreshTokens();
// [2] Load a manifest from an existing slack app to use as a template ------------
const templateManifest = await getManifest(TEMPLATE_APP_ID);
// [3] Update URLS and data in the template ------------
let devApp = { name: 'Review App', slashCommand: '/myslashcommand' };
templateManifest.settings.interactivity.request_url = `${PUBLIC_URL}/slack/events`;
templateManifest.settings.interactivity.message_menu_options_url = `${PUBLIC_URL}/slack/events`;
templateManifest.features.slash_commands[0].url = `${PUBLIC_URL}/slack/events`;
templateManifest.oauth_config.redirect_urls[0] = `${PUBLIC_URL}/slack/oauth_redirect`;
templateManifest.settings.event_subscriptions.request_url = `${PUBLIC_URL}/slack/events`;
templateManifest.display_information.name = devApp.name;
templateManifest.features.bot_user.display_name = devApp.name;
templateManifest.features.slash_commands[0].command = devApp.slashCommand;
// [5] Verify that the manifest is still valid ------------
await verifyManifest(templateManifest);
// [6] Create our new slack dev application ------------
devApp.data = await createDevApp(templateManifest);
console.log(devApp);
}
main();
Hope this helps anyone else looking to update Slack applications programatically.
No, such a method does not exist in the official documentation. There might be an unofficial method - there are quite a few of them actually - but personally I doubt it.
But you don't need this feature for developing Slack apps. Just simulate the POST calls from Slack on your local dev machine with a script and then do a final test together with Slack on your webserver on the Internet.