I am doing a project where the front-end is managed with Vue.js and the back with elixir / phoenix framework for my api.
I need to manage the authentication of my users using csrf-token and JWT.
I am using guardian library for the jwt part (https://hexdocs.pm/guardian/readme.html)
and I am currently blocking on the csrf_token part.
I want to generate a csrf-token that I will put in the jwt when sending the login form from vue.js and that will be returned when the user is connected.
I read the documentation but I don't see how to implement the csrf-token if the html page is not generated by phoenix. (https://hexdocs.pm/plug/Plug.CSRFProtection.html)
So how can I manage the generation of a csrf-token with phoenix in my vue.js html page?
Here is the route I use for the connection :
scope "/api", AuthTutorialPhoenixWeb do
pipe_through(:api)
post("/users/sign_in", SessionController, :new)
end
And this is my controller :
defmodule AuthTutorialPhoenixWeb.SessionController do
use AuthTutorialPhoenixWeb, :controller
alias AuthTutorialPhoenix.Accounts
alias AuthTutorialPhoenix.Authentication.Guardian
action_fallback(AuthTutorialPhoenixWeb.FallbackController)
# new session
def new(conn, %{"email" => email, "password" => password}) do
case Accounts.authenticate_user(email, password) do
{:ok, user} ->
# Lifetime Token of 15 minutes
{:ok, access_token, _claims} =
Guardian.encode_and_sign(user, %{}, token_type: "access", ttl: {15, :minute})
{:ok, refresh_token, _claims} =
Guardian.encode_and_sign(user, %{}, token_type: "refresh", ttl: {7, :day})
conn
|> put_resp_cookie("ruid", refresh_token)
|> put_status(:created)
|> render("token.json", access_token: access_token, user: user)
{:error, :unauthorized} ->
body = Jason.encode!(%{error: "unauthorized"})
conn
|> send_resp(401, body)
end
end
end
This is a little late to the party, but you can generate the CSRF token on demand with Plug.CSRFProtection.get_csrf_token() and send it to the front end
Related
I'm writing a Twitter client.
I have the access_token and refresh_token but I don't have the access_token_secret.
{"scope"=>"tweet.read offline.access", "expires_in"=>7200, "token_type"=>"bearer", "access_token"=>"a...E", "refresh_token"=>"NW...E", "raw_attributes"=>{"scope"=>"tweet.read offline.access", "expires_in"=>7200, "token_type"=>"bearer", "access_token"=>"aD...E", "refresh_token"=>"NW...E"}}
I would like to access the user's home timeline, how can I do that?
Here is the code for the client if it's relevant:
#client ||= TwitterOAuth2::Client.new(
identifier: credentials.client_id,
secret: credentials.client_secret,
redirect_uri: redirect_uri
)
uri = client.authorization_uri(scope: scope.split)
update!(extras: {code_verifier: client.code_verifier})
uri
client.authorization_code = code
update!(extras: client.access_token!(extras["code_verifier"]))
In my server-to-server scenario, I have an ASP.NET Core service in (Azure Active Directory) Tenant A (which I control), and I want to allow a specific (daemon) client app registration in Tenant B (which I do not control) to access my service.
I've found some examples of doing multi-tenant authorization via Role-Based Access Control but I don't want to use roles because I don't understand the security implications of allowing admins in Tenant B to decide who gets the proper "role" to talk to my app. Instead I want to decide myself, and an access control list seems like a good way to do this.
In theory I think I can use JWT Bearer Tokens issued by B, and consume them in A, but I haven't been able to get this working: depending on what I try, authentication fails on the server with 401s like
WWW-Authenticate: Bearer error="invalid_token", error_description="The issuer '(null)' is invalid"
and
WWW-Authenticate: Bearer error="invalid_token", error_description="The signature key was not found"
even though my bearer token seems to validate fine in jwt.io ("Signature verified", "iss": "https://sts.windows.net/4510468d-3790-4a1a-8209-84281b2d1596/").
My code is in a git repo and here is my startup:
type Startup(configuration: IConfiguration) =
member _.Configuration = configuration
// This method gets called by the runtime. Use this method to add services to the container.
// see https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-v2-aspnet-core-web-api for more
member _.ConfigureServices(services: IServiceCollection) =
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(fun o ->
configuration.Bind(o)
o.TokenValidationParameters <- new Microsoft.IdentityModel.Tokens.TokenValidationParameters(ValidateAudience=true)
)
.Services.AddControllers() |> ignore
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
member _.Configure(app: IApplicationBuilder, env: IWebHostEnvironment) =
if (env.IsDevelopment()) then
app.UseDeveloperExceptionPage() |> ignore
app.UseHttpsRedirection()
.UseRouting()
.UseAuthentication()
.UseAuthorization()
.UseEndpoints(fun endpoints ->
endpoints.MapControllers() |> ignore
) |> ignore
appsettings.json:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "wilsonsoft.onmicrosoft.com",
"ClientId": "88ac0449-3fae-4113-a1ba-fb4f2d041702",
"TenantId": "c4568757-6752-4ed0-a24a-b5ab2df02011"
}
}
A typical request:
GET https://localhost:44336/weatherforecast/who HTTP/1.1
Authorization: bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Im5PbzNaRHJPRFhFSzFqS1doWHNsSFJfS1hFZyIsImtpZCI6Im5PbzNaRHJPRFhFSzFqS1doWHNsSFJfS1hFZyJ9.eyJhdWQiOiJodHRwczovL3dpbHNvbnNvZnQub25taWNyb3NvZnQuY29tL0hlbGxvV2VhdGhlciIsImlzcyI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzQ1MTA0NjhkLTM3OTAtNGExYS04MjA5LTg0MjgxYjJkMTU5Ni8iLCJpYXQiOjE2MTcwMzI5NDAsIm5iZiI6MTYxNzAzMjk0MCwiZXhwIjoxNjE3MDM2ODQwLCJhaW8iOiJFMlpnWUNqYlpMKzk5TGErZWQ5blYrUEhsUmI5QUE9PSIsImFwcGlkIjoiNDE1OTA3NjMtZWM3YS00Mzc2LTkwNmYtY2VlMTM4NzExMzg0IiwiYXBwaWRhY3IiOiIxIiwiaWRwIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvNDUxMDQ2OGQtMzc5MC00YTFhLTgyMDktODQyODFiMmQxNTk2LyIsIm9pZCI6IjVkOGQ0ZTZkLTQ4MzQtNDA3Ny1hZGIyLTQ5ODNjYWQxMGY4ZiIsInJoIjoiMC5BUTBBalVZUVJaQTNHa3FDQ1lRb0d5MFZsbU1IV1VGNjdIWkRrR19PNFRoeEU0UU5BQUEuIiwic3ViIjoiNWQ4ZDRlNmQtNDgzNC00MDc3LWFkYjItNDk4M2NhZDEwZjhmIiwidGlkIjoiNDUxMDQ2OGQtMzc5MC00YTFhLTgyMDktODQyODFiMmQxNTk2IiwidXRpIjoiYWE4MDEtX2d1a21PQUpBcGFQOThBQSIsInZlciI6IjEuMCJ9.DwpWaOqoZgNoDka6-0FYQr1ivllL2taXdqtat_65x_kuT6r3uiknhL19Fu6dFmJ7UCgjc3-JZh5Bee0uZvVHbwCjZKHsUNrDEANnDkK4hGFzSKyU3NL7X9iRdPeBl3-GUSRGbPeozHTF93epSEhDyY3PkS1ICEfdAG7yi8cerBzmuy-lsUWs90sWlrWVYjDRtFWzwlovNgS6mPkx2cKlsC34WK6QXafJYqPcA5XW1EqZGyA5S0qvQS0VaheABEfkTIC8pEijieImWKIqFefd2G7blBB1Qdng4NAPcHOhmnRSiClCrwXS_5hOYsUXFS4xVAMPIZx9peWXMlk6XyQzNg
Host: localhost:44336
Response:
HTTP/1.1 401 Unauthorized
Transfer-Encoding: chunked
Server: Microsoft-IIS/10.0
WWW-Authenticate: Bearer error="invalid_token", error_description="The signature key was not found"
X-Powered-By: ASP.NET
Date: Mon, 29 Mar 2021 16:06:48 GMT
0
Can anyone advise on what this error message is trying to tell me and/or if there's a better way to accomplish my goal of authorizing a specific client in a different tenant, in ASP.NET Core?
The answer: in multi-tenant scenarios, you must authenticate against the "common" endpoint AND ALSO use a custom TokenValidationParameters.IssuerValidator that transforms the "iss" claim before checking it. That will allow you to authenticate with the jwt token. See https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-v2-aspnet-core-web-api for more details.
Then, authorization is a straightforward matter of checking the app id claim.
// This method gets called by the runtime. Use this method to add services to the container.
member _.ConfigureServices(services: IServiceCollection) =
IdentityModelEventSource.ShowPII <- true
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(fun jwtOptions ->
jwtOptions.TokenValidationParameters <-
new Microsoft.IdentityModel.Tokens.TokenValidationParameters(
ValidateAudience = true,
ValidateIssuer = true
)
jwtOptions.Authority <- "https://login.microsoftonline.com/common"
jwtOptions.Audience <- "https://wilsonsoft.onmicrosoft.com/HelloWeather"
jwtOptions.TokenValidationParameters.IssuerValidator <-
fun issuer jwt tokenValidationParams ->
// In order to support multi-tenant auth, replace {tenantid} placeholder with actual tenantId,
// e.g. "https://sts.windows.net/{tenantid}/" becomes "https://sts.windows.net/{c4568757-6752-4ed0-a24a-b5ab2df02011}/"
// See https://thomaslevesque.com/2018/12/24/multitenant-azure-ad-issuer-validation-in-asp-net-core/ for more
let validatedIssuer =
match jwt with
| :? JwtSecurityToken as jwt ->
match jwt.Payload.TryGetValue("tid") with
| true, (:? string as tenantId) ->
let validIssuers =
(Seq.append tokenValidationParams.ValidIssuers [tokenValidationParams.ValidIssuer])
|> Seq.filter (System.String.IsNullOrEmpty >> not)
|> Seq.map (fun i -> i.Replace("{tenantid}", tenantId))
validIssuers |> Seq.tryFind (fun i -> i = issuer)
| _ -> None
| _ -> None
match validatedIssuer with
| Some i -> i
| None ->
$"IDX10205: Issuer validation failed. Issuer: '{issuer}'. Did not match: validationParameters.ValidIssuer: '{tokenValidationParams.ValidIssuer}' or validationParameters.ValidIssuers: '{tokenValidationParams.ValidIssuers}'."
|> SecurityTokenInvalidIssuerException
|> raise
)
.Services.AddAuthorization(fun authzOptions ->
// only allow specific appIds (from trusted issuers) access to this app
authzOptions.AddPolicy(
"ACL",
(fun policy ->
policy.RequireAssertion(fun ctx ->
ctx.User.HasClaim(fun claim ->
claim.Type = "appid" && claim.Value = "083d3ba2-ed4e-4e11-b7ef-d8cc46ffe346")
)
|> ignore
))
)
.AddControllers() |> ignore
Fixed version.
I'm attempting to get Guardian auth work for my application. But I'm completely stuck and can't find any support for the problem I'm having.
As far as I know I've setup Guardian exactly how the documentation shows how, but when I test authentication in the browser it fails on EnsureAuthenticated plug that Guardian provides.
Here is what I'm working with:
CONFIG:
All values are filled correctly in the app.
config :statcasters, MyApp.Guardian,
allowed_algos: ["HS512"],
verify_module: Guardian.JWT,
issuer: "my_app",
ttl: {30, :days},
allowed_drift: 2000,
verify_issuer: true,
secret_key: "my_secret_key"
AUTHENTICATED CONTROLLER:
defmodule Statcasters.LeagueController do
use StatcastersWeb, :controller
alias Statcasters.{League, Repo}
plug Guardian.Plug.EnsureAuthenticated
def create(conn, %{"league" => league_params}) do
changeset = League.changeset(%League{}, league_params)
case Repo.insert(changeset) do
{:ok, league} ->
conn
|> put_status(:created)
|> render("league.json", league: league)
{:error, changeset} ->
conn
|> put_status(:unprocessable_entity)
|> render(Statcasters.ChangesetView, "error.json", changeset: changeset)
end
end
end
In this controller is where it fails. When it goes to the EnsureAuthenticated plug it halts right there. but I have a valid JWT in the headers at this point.
Here our my params:
Parameters: %{"headers" => %{"Authorization" => "Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJTdGF0Y2FzdGVycyIsImV4cCI6MTUyNzUzMDA1OSwiaWF0IjoxNTI0OTM4MDU5LCJMiOiJTdGF0Y2FzdGVycyIsImp0aSI6IjJhNDg3MWQ4LTkwZGEtNDNlYS1hMGJlLWVjNjgwNjIzOTBkOCIsIm5iZiI6MTUyNDkzODA1OCwic3ViIjoiMSIsInR5cCI6InJlZnJlc2gifQ.EKeaHoQiW9tmtsabPIjj6069zD6Vcex9w3xfkXP5MIyiogWh400S6wMzaAsTQd20I5ai_y9jJTtgLzqYfbGTaQ"}
I've verified that the JWT is valid here.
REQUEST:
axios.post('/api/v1/leagues', {
league: {
name: this.$refs.league_name.value,
player_limit: this.$refs.player_limit.value,
},
headers: {
Authorization: "Bearer jwt(correct jwt)"
}
}).then(response => {
}).catch(error => {
})
Again, the problem is that my auth is failing in the Plug.EnsureAuthenticated hook. But I can't understand why because I seem to be setting everything up correctly and the JWT is in the auth header.
You're sending the header as a POST parameter, not an HTTP header. You need to put the headers in the third argument for axios.post:
axios.post('/api/v1/leagues', {
league: {
name: this.$refs.league_name.value,
player_limit: this.$refs.player_limit.value,
}
}, {
headers: {
Authorization: "Bearer jwt(correct jwt)"
}
})
I’m trying to use Openmaize for user authentication, and having trouble getting phoenix pass a token when a user logs in. It appears that no token is assigned and passed to the client, and therefore Phoenix.Token.verify fails.
IO.inspect(socket) in UserSocket.connect returns this.
Phoenix.Socket{assigns: %{}, channel: nil, channel_pid: nil,
endpoint: SeatSaver.Endpoint, handler: SeatSaver.UserSocket, id: nil,
joined: false, pubsub_server: SeatSaver.PubSub, ref: nil,
serializer: Phoenix.Transports.WebSocketSerializer, topic: nil,
transport: Phoenix.Transports.WebSocket, transport_name: :websocket,
transport_pid: #PID<0.2098.0>}
I defined set_current_user(user, conn) function in authenticate.ex controller that looks like
defp set_current_user(user, conn) do
token = Phoenix.Token.sign(conn, "user socket", user.id)
conn
|> assign(:current_user, user)
|> assign(:user_token, token)
In the app.html.eex, the following has been added.
<script> window.userToken = “<%= assigns[:user_token] %>” </script>
<script src = “<%= static_path(#conn, “/js/app.js”) %>”></script>
in the app.js,
let socket = new Socket(”/socket”, {
params: {token: window.userToken},
…
})
and finally, user_socket.ex has
def connect(%{"token" => token}, socket) do
case Phoenix.Token.verify(socket, "user socket",
token, max_age: #max_age) do
{:ok, user_id} ->
IO.inspect(user_id)
{:ok, assign(socket, :user_id, user_id)}
{:error, _reason} ->
:error # this errors out because token is nil
end
end
First you need to add secret_key_base in config/config.exs.
secret_key_base: xxxxx
I am using Friend to build authentication into a Compojure web application.
I have defined a bespoke authentication workflow for Friend:
(defn authentication-workflow []
(routes
(GET "/logout" req
(friend/logout* {:status 200}))
(POST "/login" {{:keys [username password]} :params}
(if-let [user-record (authenticate-user username password)]
(workflows/make-auth user-record {:cemerick.friend/workflow :authorisation-workflow})
{:status 401}))))
The authentication part is factored out:
(defn authenticate-user [username password]
(if-let [user-record (get-user-for-username username)]
(if (creds/bcrypt-verify password (:password user-record))
(dissoc user-record :password))))
This works, but...
I am using AngularJS and having to post request parameters leads to some ugly Angular code (cribbed elsewhere from a StackOverflow answer):
$http({
method: 'POST',
url: '/login',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
transformRequest: function(obj) {
var str = [];
for (var p in obj)
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
return str.join("&");
},
data: {
username: username,
password: password
}
});
I would much rather do this much simpler call instead and just post a JSON object via the request body:
$http.post('/login', {username: username, password: password})
I tried to use ":body" in the authentication handler instead of ":params" but the value of :body seemed neither JSON nor Clojure to me so I don't know how I can use it:
{username me#myapp.com, password password}
I already have JSON request/response mapping workflows working correctly for my REST API handlers, and I checked already that the request headers (e.g. ContentType) were correct for JSON.
So can this be done with Compojure/Friend, and if so how?
Here is some working code and an explanation...
First the Friend workflow, using the request body:
(defn authentication-workflow []
(routes
(GET "/logout" req
(friend/logout* {:status 200}))
(POST "/login" {body :body}
(if-let [user-record (authenticate-user body)]
(workflows/make-auth user-record {:cemerick.friend/workflow :authorisation-workflow})
{:status 401}))))
Second, the authentication function:
(defn authenticate-user [{username "username" password "password"}]
(if-let [user-record (get-user-for-username username)]
(if (creds/bcrypt-verify password (:password user-record))
(dissoc user-record :password))))
Third, the Compojure application with middlewares declared:
(def app
(-> (handler/site
(friend/authenticate app-routes
{:workflows [(authentication-workflow)]}))
(params/wrap-keyword-params)
(json/wrap-json-body)
(json/wrap-json-response {:pretty true})))
Finally a fragment of AngularJS code to post the credentials (username and password come from an AngularJS model):
$http.post('/login', {username: username, password: password});
So what happens is this...
The Angular javascript code posts JSON to the web application login URL. The "Content-Type" header is automatically set to "application/json" and the request body is automatically encoded as JSON, for example:
{"username":"batman#batcave.org","password":"tumblerrocks"}
On the server, the middleware parses the JSON to a Clojure map and presents it to the handler via the ":body" keyword:
{username batman#batcave.org, password tumblerrocks}
The request is then routed to the custom Friend authentication workflow.
Finally the submitted values are extracted from the Clojure map and used to authenticate the user.
I suspect that your wrappers are applied in the wrong order. Check that ring.middleware.json/wrap-json-body is applied before (outside of) the friend wrapper.
e.g.
(def my-handler (wrap-json-body (cemerick.friend/authenticate ...)))
Otherwise, a quick fix might be to just wrap your whole app in ring.middleware.json/wrap-json-params