brillout / wildcard-api

Functions as API.
MIT License
368 stars 14 forks source link

[Question] How to deal with header variables like Authorization? #39

Closed johnsonthedev closed 4 years ago

johnsonthedev commented 4 years ago

Hi, in graphql I have a cookie with my apollo-token and check for it in my context.

export default async ({ req, connection }) => {

  const authToken = req.get("Authorization")
  let user = authToken ? await tradeTokenForUser(authToken) : null

  return {
    user
  }
}

You wrote that we can setup a middleware in express for reading the token: app.use(wildcard(getContext));

This makes sense for me but I am note sure where I have to set my token in the wildcard client to send it with each endpoint query. Can you help here ? Thx!!

brillout commented 4 years ago

Hey!

You can achieve that with getContext:

const express = require('express')
const wildcard = require('@wildcard-api/server/express')

const app = express()

// We install the Wildcard middleware
app.use(wildcard(getContext))

// `getContext` is called on every API request. It defines the `context` object.
// `req` is Express' request object
async function getContext(req) {

  const authToken = req.get("Authorization")
  let user = authToken ? await tradeTokenForUser(authToken) : null

  const context = {user}
  return context
}

You can think of the context object as bridge between Express and your Wildcard functions.

The getContext function is called on every API request and you can use it to provide any information you want to your Wildcard functions.

Does that make sense?

johnsonthedev commented 4 years ago

Hi! Sry, I didn't express myself well.

I got the backend piece working but struggle to attach my authorization header to all endpoint requests.

Basically, I am looking for a way to attach the token to wildcard to make all requests have that token as an Authorization header without having to manually attach it to every request in the action.

I haven't worked with RPC before. So it could be that I am missing an option that can do this for me. With axios I used an interceptor to attach it to all requests:

axios.interceptors.request.use(function (config) {
    const token = store.getState().session.token;
    config.headers.Authorization =  token;

    return config;
});

In graphql I added it to the httpLink

const httpLink = createHttpLink({ uri: 'http://localhost:3000/graphql' })

const authLink = setContext((_, { headers }) => {
  const token = Cookies.get('token')

  return {
    headers: {
      ...headers,
      authorization: `Bearer ${token}`
    }
  }
})

const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache()
})

I am looking to do something similar with wildcard.

Here is a small app I created to make it more visual for you:

Backend:

endpoints.js

import service from './service'
const {endpoints} = require('@wildcard-api/server');

const jwt = require('jsonwebtoken');
const jwtSecret = "mysuperdupersecret"; 

endpoints.me = function() {
  return this.user
};

endpoints.login = function(params) {
const user = service.login(params)

if (user) {
    const token = jwt.sign({ user }, jwtSecret, { expiresIn: 60 }) // 1 min token
    return token
}

return null
};

index.js

const express = require('express')
const wildcard = require('@wildcard-api/server/express')

const jwt = require('jsonwebtoken');
const jwtSecret = "mysuperdupersecret"; 

const app = express()
const port = 8000

app.use((req, res, next) => {
    res.setHeader("Access-Control-Allow-Origin", "*")
    res.setHeader("Access-Control-Allow-Methods", "POST,GET,OPTIONS")
    res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization")
    if (req.method === "OPTIONS") {
      return res.sendStatus(200)
    }
    next()
  })

  app.use(wildcard(getContext))
  app.listen(port)

async function getContext(req) {

  const authToken = req.get("Authorization")
  let user

  if (authToken) {
    user = jwt.verify(token, jwtSecret);
  }

  return  {user}
}

frontend

wildcard.js

import wildcardClient, { endpoints } from "@wildcard-api/client";
wildcardClient.serverUrl = "http://localhost:8000";

export default { wildcardClient, endpoints };

index.js

import React, { useState, useEffect } from 'react';
import { Cookies } from 'react-cookie';
import {endpoints} from '../wildcard';

const cookies = new Cookies();

export default function Index() {
  const [user, setUser] = useState(0);

  const loadUser = async () => {
    setUser(await endpoints.me() ||  'Guest')
  }

  useEffect(() => {
    loadUser()
  }, []);

  const login = async () => {
    const token = await endpoints.login({email:'my@email.de', password:'password'})

    if (token) {
      cookies.set('token', token);
      loadUser()
    } 
  }

  return (
    <div>
      <p>Hello {user} </p>
      <button onClick={login}>Login</button>
    </div>
  );
}

Hope this makes more sense to you

brillout commented 4 years ago

Wildcard doesn't have any utility to set cookies.

To process login and logout you should bypass Wildcard and use Express directly.

Or you can set cookies in the browser, which is what you are doing, but this is unusual and I'd stick to the more traditional approach of using Express directly, unless you have a good reason to deviate from the traditional approach.

Once the cookies are set you use getContext to bridge over to Wildcard.

As usual, let me know if that makes sense :-).

brillout commented 4 years ago

Btw. I'm exploring ways for allowing Wildcard users to modify the context object in a protocol-agnostic and secure way.

const {endpoints, setPrivateKey} = require('@wildcard-api/server');

endpoints.login = function(params) {
  const user = service.login(params)

  if (user) {
    // We modify the context!
    this.user = user;
  }
};

endpoints.getAllTrelloCards = function() {
  const {user} = this;
  // SQL/ORM query to get all user's Trello cards
};

// The context is secure.
// Which means you can do `this.user = {username: 'brillout'};` and
// safely assume that the user is indeed `brillout`.
// (That is, the client cannot modify the context.)
// To secure the context, Wildcard uses a private key that you provide.
setPrivateKey('AvnrrbAZ7pxH6P0s38');

// That's it!
//  - No need for `getContext`.
//  - Entirely transport protocol agnostic. No need to fiddle around with
//    HTTP headers, cookies, or localStorage.
//  - Wildcard automatically makes your context secure.
//  - Cookies are going to be deprecated soon, this is future-proof.

What do you think?

johnsonthedev commented 4 years ago

Ok, I only know the REST and GraphQl way with attaching the JWT token as an Authorization Header to each request.

Especially with SSR like Next.js / Nuxt.js; That is why I am sticking to JWT.

I can't think of any other solution dealing with authorizations but if you find one I am happy to test it out :-)

brillout commented 4 years ago

I'll show you once I've implemented the new context design. I believe you'll love it ;-).

brillout commented 4 years ago

Let me know if you are stuck btw. Your solution of setting the cookie on the browser side works for you now, correct?

johnsonthedev commented 4 years ago

kind of. I am just sending the auth token as first parameter to all of my endpoints. It's a step backwards compared to REST/GraphQL, but it works for now.

Anyway, I decided to work on frontend first and wait what you come up with. Still can't really imagine it :-D

johnsonthedev commented 4 years ago

Hey @brillout ,

Following up on our twitter conversation I created this snippet. It's of course not production ready and also my first steps with React.. So please bare with me :-D

But I feel it shows what I am doing.

I am always sending the token as first argument to all my endpoints. Before running the endpoint on the server I call an intercepter to remove the token argument and forward the rest to the endpoint.

If you have a better idea to do authentication with node.js and next.js, let me know. Important

you have to add this line to the RunEndpoint function in the WildcardApi file in the node_modules repo.. I know it is super evil... again, just for showing.

[endpointName, endpointArgs, context, isDirectCall] = await require('../../../context')(endpointName, endpointArgs, context, isDirectCall)

https://github.com/johnson544/doorman-react

https://github.com/johnson544/doorman-node

brillout commented 4 years ago

I will have a look at it tmrw :)

brillout commented 4 years ago

I had a look at it and I'll reply tmrw ;-).

brillout commented 4 years ago

In general authorization cookies should be handled only by the server.

In your case it is only Express (or whatever server library Next.js uses) that should modify and read the cookie. You don't have to do this yourself and you can use a library instead, there are many Express auth libraries. Social login libraries usually handle this for you.

Do you really need SSR? If you don't need SSR then I'd recommend to not use Next.js but Parcel instead.

If you do need SSR then you'll need to re-bind the cookie header while doing SSR, see https://github.com/reframejs/wildcard-api/blob/master/docs/ssr-auth.md#ssr--authentication

Does that point you to the right direction?

michie1 commented 4 years ago

Wildcard doesn't have any utility to set cookies.

To process login and logout you should bypass Wildcard and use Express directly.

I'm sharing a helper function via getContext that sets a cookie. What do you think of this approach? @brillout

The getContext middleware function does not have the second parameter response, but you still can use request.res?.cookie. It feels a bit hacky though to use.

brillout commented 4 years ago

Hi @michie1,

Yes exactly, getContext is not meant to be used to manipulate the response.

Auth is usually done outside of Wildcard and is done by the server framework instead. For example:

const express = require("express");
const cookieParser = require("cookie-parser");
const computeJwtToken = require('./path/to/computeJwtToken');

const app = express();
app.use(cookieParser());

app.get("/login", (req, res) => {

  const options = {
    maxAge: 7 * 24 * 60 * 60 * 1000, // Expires after 7 days
    httpOnly: true, // The cookie is only accessible by the web server
  };
  const jwt = computeJwtToken(req);;
  res.cookie("Authorization", jwt, options);

  res.send("login successfull");
});

You can then read the Authorization cookie in Wildcard's getContext.

I've plans to implement a new API setContext which I'll implement if the demand for it continuous to increase.

In the meantime if misusing getContext to manipulate the response works for you, then go for it. As far as I can see, there shouldn't be any problem.

Let me know if you have other questions!

brillout commented 4 years ago

Some news about this at https://github.com/reframejs/wildcard-api/issues/59.