How to add basic auth to Scotty middleware? - authentication

I'm currently making a Scotty API and I couldn't find any examples of basicAuth implementations (Wai Middleware HttpAuth).
Specifically, I want to add basic auth headers (user, pass) to SOME of my endpoints (namely, ones that start with "admin"). I have everything set up, but I can't seem to make the differentiation as to which endpoints require auth and which ones don't. I know I need to use something like this, but it uses Yesod, and I wasn't able to translate it to Scotty.
So far, I have this:
routes :: (App r m) => ScottyT LText m ()
routes = do
-- middlewares
middleware $ cors $ const $ Just simpleCorsResourcePolicy
{ corsRequestHeaders = ["Authorization", "Content-Type"]
, corsMethods = "PUT":"DELETE":simpleMethods
}
middleware $ basicAuth
(\u p -> return $ u == "username" && p == "password")
"My Realm"
-- errors
defaultHandler $ \str -> do
status status500
json str
-- feature routes
ItemController.routes
ItemController.adminRoutes
-- health
get "/api/health" $
json True
But it adds authentication to all my requests. I only need it in some of them.
Thank you so much!

You can use the authIsProtected field of the AuthSettings to define a function Request -> IO Bool that determines if a particular (Wai) Request is subject to authorization by basic authentication. In particular, you can inspect the URL path components and make a determination that way.
Unfortunately, this means that the check for authorization is completely separated from the Scotty routing. This works fine in your case but can make fine-grained control of authorization by Scotty route difficult.
Anyway, the AuthSettings are the overloaded "My Realm" string in your source, and according to the documentation, the recommended way of defining the settings is to use the overloaded string to write something like:
authSettings :: AuthSettings
authSettings = "My Realm" { authIsProtected = needsAuth }
That looks pretty horrible, but anyway, the needsAuth function will have signature:
needsAuth :: Request -> IO Bool
so it can inspect the Wai Request and render a decision in IO on whether or not the page needs basic authentication first. Calling pathInfo on the Request gives you a list of path components (no hostname and no query parameters). So, for your needs, the following should work:
needsAuth req = return $ case pathInfo req of
"admin":_ -> True -- all admin pages need authentication
_ -> False -- everything else is public
Note that these are the parsed non-query path components, so /admin and /admin/ and /admin/whatever and even /admin/?q=hello are protected, but obviously /administrator/... is not.
A full example:
{-# LANGUAGE OverloadedStrings #-}
import Web.Scotty
import Network.Wai.Middleware.HttpAuth
import Data.Text () -- needed for "admin" overloaded string in case
import Network.Wai (Request, pathInfo)
authSettings :: AuthSettings
authSettings = "My Realm" { authIsProtected = needsAuth }
needsAuth :: Request -> IO Bool
needsAuth req = return $ case pathInfo req of
"admin":_ -> True -- all admin pages need authentication
_ -> False -- everything else is public
main = scotty 3000 $ do
middleware $ basicAuth (\u p -> return $ u == "username" && p == "password") authSettings
get "/admin/deletedb" $ do
html "<h1>Password database erased!</h1>"
get "/" $ do
html "<h1>Homepage</h1><p>Please don't <a href=/admin/deletedb>Delete the passwords</a>"

Related

How to get API call origin in NextJS API endpoint

I have an API set up that receives a token, and I want to store that token in a database. But I also want to store the origin URL.
Let's say my API endpoint is located at https://myapp.com/api/connect
Now, I want to send a token from my website https://mywebsite.net
After I send a token, I want to be able to store the token and the website URL to the database in NextJS code.
My endpoint would store this info to the database:
{
token: someRandomToken
origin: https://mywebsite.net
}
I tried logging the whole req object from the handler to see if that info exist but the console log fills my terminal fast.
Inside Next's Server-Side environment you have access to req.headers.host as well as other headers set by Vercel's or other platforms' Reverse Proxies to tell the actual origin of the request, like this:
/pages/api/some-api-route.ts:
import { NextApiRequest } from "next";
const LOCAL_HOST_ADDRESS = "localhost:3000";
export default async function handler(req: NextApiRequest) {
let host = req.headers?.host || LOCAL_HOST_ADDRESS;
let protocol = /^localhost(:\d+)?$/.test(host) ? "http:" : "https:";
// If server sits behind reverse proxy/load balancer, get the "actual" host ...
if (
req.headers["x-forwarded-host"] &&
typeof req.headers["x-forwarded-host"] === "string"
) {
host = req.headers["x-forwarded-host"];
}
// ... and protocol:
if (
req.headers["x-forwarded-proto"] &&
typeof req.headers["x-forwarded-proto"] === "string"
) {
protocol = `${req.headers["x-forwarded-proto"]}:`;
}
let someRandomToken;
const yourTokenPayload = {
token: someRandomToken,
origin: protocol + "//" + host, // e.g. http://localhost:3000 or https://mywebsite.net
};
// [...]
}
Using Typescript is really helpful when digging for properties as in this case. I couldn't tell if you are using Typescript, but in case you don't, you'll have to remove NextApiRequest.

Trying to make haskell warp-tls authenticate the client

I'm trying to make a https server that authenticates the client but it serves curl --verbose --cacert ca.crt https://localhost:3443 successfully. I think it should reject for lack of client creds and require curl --verbose --cert client.crt --key client.key --cacert ca.crt https://localhost:3443. Here's the code:
{-# LANGUAGE DataKinds, ScopedTypeVariables, TypeOperators #-}
module MockServerMain where
import Network.Wai
import Network.Wai.Handler.Warp
import Network.Wai.Handler.WarpTLS
import Servant
import Network.TLS
type MyApi = Get '[JSON] String
api :: Proxy MyApi
api = Proxy
server :: Server MyApi
server = return "Hello from Haskell!"
app :: Application
app = serve api server
main :: IO ()
main =
let
stngs = mkSettings "localhost.crt" "ca.crt" "localhost.key"
warpOpts = setPort 3443 defaultSettings
in
runTLS stngs warpOpts app
mkSettings :: FilePath -> FilePath -> FilePath -> TLSSettings
mkSettings crtFile chainFile keyFile = do
let
hooks = def
{ onClientCertificate = \_ -> return CertificateUsageAccept
}
tlsSs = ( tlsSettingsChain crtFile [chainFile] keyFile )
{ tlsServerHooks = hooks
, tlsWantClientCert = True
}
in tlsSs
I suppose the obvious explanation is that I told it to always accept in the onClientCertificate hook, but the docs say I'm not expected to validate. I know that the tlsWantClientCert is having an effect because if I comment out the hooks it fails. When I only used tlsSettings rather than tlsSettingsChain, the behaviour was the same: accepting the credless curl. I know that the certs are OK because I can make nginx do what this haskell server is supposed to do and allow curls only with creds.
What am I supposed to do with the CA certificate that I want this server to check clients against? This server isn't supposed to know the CA private key.
I think this is the root of the problem: where's the constructor taking ServerParams:
data TLSSettings
= TLSSettingsSimple
{ settingDisableCertificateValidation :: Bool
, settingDisableSession :: Bool
, settingUseServerName :: Bool
}
| TLSSettings TLS.ClientParams
deriving (Show)

python3 - can't pass through autorization

I need to build webcrawler for internal usage and I need to login into administration area. I'm trying to use requests lib, tried this ways:
import urllib.parse
import requests
base_url = "https://target.url"
data = ({'login': 'login', 'pass': 'password'})
params = urllib.parse.urlencode(data)
r = requests.post(base_url, data=params)
print(r.text)
and
import requests
base_url = "https://target.url"
r = requests.post(base_url, auth=('login', 'password')
print(r.text)
but in both cases r.text returns me login page content same as if I try to get any other page after auth code:
req = requests.get("https://target.url/smth")
What I lose sight of? I have ideas:
chain of hidden redirections from https://target.url to real login page, so I send auth info to wrong url
I don't send additional required info (like cookies e.g.)
Could you please comment? How can I gather required for login information?
In my case problem was in 'Referer' parameter in headers, which is required but wasn't specified

Symfony REST API authentication without sfGuardPlugin

I'm trying to find information on securing a HTTP REST API in a Symfony project, but all I can find is information about using sfGuardPlugin. From what I can see, this plugin isn't very useful for web services. It tries to have user profile models (which aren't always that simple) and have "sign in" and "sign out" pages, which obviously are pointless for a stateless REST API. It does a lot more than I'll ever have need for and I what to keep it simple.
I want to know where to implement my own authorisation method (loosely based on Amazon S3's approach). I know how I want the authorisation method to actually work, I just don't know where I can put code in my Symfony app so that it runs before every request is processed, and lets approved requests continue but unsuccessful requests return a 403.
Any ideas? I can't imagine this is hard, I just don't know where to start looking.
There is a plugin for RESTful authentication -> http://www.symfony-project.org/plugins/sfRestfulAuthenticationPlugin
Not used it though ....
How where you planning to authenticate users ?
The jobeet tutorial uses tokens ... http://www.symfony-project.org/jobeet/1_4/Doctrine/en/15
I ended up finding what I was looking for by digging into the code for sfHttpAuthPlugin. What I was looking for was a "Filter". Some details and an example is described in the Askeet sample project.
Stick a HTTP basicAuth script in your <appname>_dev.php (Symfony 1.4 =<) between the project configuration "require" and the configuration instance creation.
Test it on your dev. If it works, put the code in your index.php (the live equivalent of <appname>_dev.php) and push it live.
Quick and dirty but it works. You may want to protect that username/password in the script though.
e.g.
$realm = 'Restricted area';
//user => password
$users = array('username' => 'password');
if (empty($_SERVER['PHP_AUTH_DIGEST'])) {
header('HTTP/1.1 401 Unauthorized');
header('WWW-Authenticate: Digest realm="'.$realm.
'",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"');
die('Text to send if user hits Cancel button');
}
// || !isset($users[$data['username']]
// analyze the PHP_AUTH_DIGEST variable
if (!($data = http_digest_parse($_SERVER['PHP_AUTH_DIGEST'])) || !isset($users[$data['username']])) {
header('HTTP/1.1 401 Unauthorized');
header('WWW-Authenticate: Digest realm="'.$realm.
'",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"');
die('Wrong Credentials!');
}
// generate the valid response
$A1 = md5($data['username'] . ':' . $realm . ':' . $users[$data['username']]);
$A2 = md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']);
$valid_response = md5($A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2);
if ($data['response'] != $valid_response) {
header('HTTP/1.1 401 Unauthorized');
header('WWW-Authenticate: Digest realm="'.$realm.
'",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"');
die('Wrong Credentials!');
}
// function to parse the http auth header
function http_digest_parse($txt)
{
// protect against missing data
$needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1);
$data = array();
$keys = implode('|', array_keys($needed_parts));
preg_match_all('#(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))#', $txt, $matches, PREG_SET_ORDER);
foreach ($matches as $m) {
$data[$m[1]] = $m[3] ? $m[3] : $m[4];
unset($needed_parts[$m[1]]);
}
return $needed_parts ? false : $data;
}
// ****************************************************************************
// ok, valid username & password.. continue...

HTTP Authentication in Python

Whats is the python urllib equivallent of
curl -u username:password status="abcd" http://example.com/update.json
I did this:
handle = urllib2.Request(url)
authheader = "Basic %s" % base64.encodestring('%s:%s' % (username, password))
handle.add_header("Authorization", authheader)
Is there a better / simpler way?
The trick is to create a password manager, and then tell urllib about it. Usually, you won't care about the realm of the authentication, just the host/url part. For example, the following:
password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
top_level_url = "http://example.com/"
password_mgr.add_password(None, top_level_url, 'user', 'password')
handler = urllib2.HTTPBasicAuthHandler(password_mgr)
opener = urllib2.build_opener(urllib2.HTTPHandler, handler)
request = urllib2.Request(url)
Will set the user name and password to every URL starting with top_level_url. Other options are to specify a host name or more complete URL here.
A good document describing this and more is at http://www.voidspace.org.uk/python/articles/urllib2.shtml#id6.
Yes, have a look at the urllib2.HTTP*AuthHandlers.
Example from the documentation:
import urllib2
# Create an OpenerDirector with support for Basic HTTP Authentication...
auth_handler = urllib2.HTTPBasicAuthHandler()
auth_handler.add_password(realm='PDQ Application',
uri='https://mahler:8092/site-updates.py',
user='klem',
passwd='kadidd!ehopper')
opener = urllib2.build_opener(auth_handler)
# ...and install it globally so it can be used with urlopen.
urllib2.install_opener(opener)
urllib2.urlopen('http://www.example.com/login.html')