accounts icon indicating copy to clipboard operation
accounts copied to clipboard

Support cookie based storage for JWT's to avoid XSS

Open ElectricCookie opened this issue 7 years ago • 29 comments

This library is using the localStorage to store the JWT. This makes it accessible to the JS-Enviroment and therefore vulnerable to XSS-Attacks. A good alternative, that prevents this flaw is to use a cookie that is httpOnly.


Ressources on this problem:

https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage

Cookies, when used with the HttpOnly cookie flag, are not accessible through JavaScript, and are immune to XSS.


https://www.owasp.org/index.php/HTML5_Security_Cheat_Sheet

"Do not store session identifiers in local storage as the data is always accesible by JavaScript. Cookies can mitigate this risk using the httpOnly flag."

To make the claims available to the client, they should be sent seperately, but the whole JWT should not be available to the JS-Enviroment

ElectricCookie avatar Mar 19 '17 14:03 ElectricCookie

It's true but modern framework like angular and react auto sanitize the components rendered and prevent the xss attack.

pradel avatar Mar 24 '17 10:03 pradel

But an infected npm package or something similar would simply bypass any framework...

Especially since everything requires like 1000 dependencies, how easy would it be to implement some infected code? Not hard if you ask me.

ElectricCookie avatar Mar 24 '17 13:03 ElectricCookie

Maybe instead you could use the same strategy as ooth client, i.e. use JWTs only to create a cookie-based session. The JWT then is discarded immediately after it's used, making XSS harder.

nickredmark avatar Mar 30 '17 12:03 nickredmark

@nmaro we are currently thinking about adding cookie sessions support and use jwt only to obtain that cookie yeah

pradel avatar Mar 30 '17 13:03 pradel

@pradel @ElectricCookie @nmaro Currently the only problem with cookies is using this package on react-native apps. The RN runtime does not have the concept of cookies natively and while there is a package that implements that, it is quite unstable. I save session tokens on the AsyncStorage which has basically the same API as localStorage but async instead of sync.

As react native runtime is quite different than the browser in terms of it cannot just load and run html or js files from the network (except from webview which has it's own sandbox), I think the security flaw is much less apparent over there (at least for non jailbroken devices).

We need to support both strategies IMO.

Also @nmaro, with ooth you set out to implement a good authorization microservice for a microservice inspired system right? Then the JWT granted by ooth should be exchanged with a session cookie on the request to the resource microservice? This means that every resource service needs it's own cookie strategy added? (not a big deal of course but still a concern)

If I am mistaken by my assumptions, why respond with JWT in the first place?

davidyaha avatar Apr 01 '17 14:04 davidyaha

For reference adding an old post written by MDG on that subject. Particularly the last part is interesting as it describes a possible enhancment to storing access token in the localStorage.

davidyaha avatar Apr 02 '17 18:04 davidyaha

Also to be fair, a counter post

davidyaha avatar Apr 02 '17 18:04 davidyaha

Maybe part of our problem is the lack of browser-policy package

davidyaha avatar Apr 02 '17 18:04 davidyaha

Hi @davidyaha to quickly answer your question: yes, you are correct. If you use ooth as an independent microservice, every resource service needs its own cookie strategy. The relevant example would be graphql-api-with-auth. I also extended ooth to be usable within an express-based API (i.e. not as an external microservice, see graphql-api-with-ooth). In that case no JWT is needed at all. It's a matter of tradeoffs.

nickredmark avatar Apr 03 '17 11:04 nickredmark

Yeah I understand completely. Thanks for your answer. I hope to get on top of this issue in the next couple of weeks. It's really a matter we cannot decide on easily as we want to support as many environments as we can.

Our main focus has always been to have a truly reusable package. By now it seems to be working as @dotansimha and I have used for two projects by now, and we want to use it for another client at least in next month or so.

davidyaha avatar Apr 03 '17 17:04 davidyaha

@davidyaha is it fixed?

veeramarni avatar Jul 09 '17 20:07 veeramarni

Hey @veeramarni, We currently have no built in support for cookies to avoid XSS attacks.

The solution we would seek IMO would be to be able to turn off storage of JWT's in the localStorage and prevent sending to the client. Then get a resume session cookie sent to the client with httpOnly and secure flags on for any re-login purposes. Server side should have support for both bearer and cookie headers and should have the option to turn off each of them.

At the moment our projects does not require this. We would love to merge a PR if you'd like to tackle this one.

Thanks ahead!

davidyaha avatar Jul 11 '17 18:07 davidyaha

In the case in which the tokens are set in the cookies, is there a reason for sending only the access token ? Where would you store the refresh token otherwise ?

https://github.com/js-accounts/rest/issues/27

Aetherall avatar Sep 12 '17 12:09 Aetherall

@Aetherall No you are probably right. The only thing I would as I've written here before, is a way to turn on and off cookie storage and localStorage. This way we can support all kinds of use cases. One consideration for example are native mobile apps that usually does not use cookies.

Do you think you have enough information to draft a solution? I'd love to get this ball rolling. If you do, try branching out from our TS PR.

Thanks!

davidyaha avatar Sep 13 '17 08:09 davidyaha

Alright, I'll write here if I need to !

Aetherall avatar Sep 13 '17 18:09 Aetherall

I'm not going to talk about security right now, I prefer to do so in the community meeting. What I would like to talk about right now is: how do you plan to deal with server side rendering if you don't use cookies at all? How will the server recognize you without further interaction? Unfortunately cookies are the only way.

darkbasic avatar Nov 03 '17 21:11 darkbasic

You are right! This is actually quite easy to add on top of the current js-accounts implementation but should probably be part of this package as well. Excited to meet you @darkbasic and hope to see more people that have previously shown interest or opened issues on this project!

davidyaha avatar Nov 03 '17 21:11 davidyaha

My pleasure @davidyaha, I'm sure it will be a great talk.

darkbasic avatar Nov 03 '17 21:11 darkbasic

I know we managed to swap LocalStorage with cookies when trying to do the nextJS example, maybe this is possible now (to support cookies) and should be a separate package so one can choose? I love the modularity of the project. and cookies might be out of vogue, but are still pretty much useful in some use-cases.

agustif avatar Apr 26 '20 13:04 agustif

https://www.accountsjs.com/docs/api/express-session/index/#accountsexpress-session

Does using express-session middlewear end up accommodating this? Wrapping this around GraphQL should enable the use of headers / GQL context instead of the JWT on the client side right?

Any advice appreciated, finding my around this lib. thanks!

bmitchinson avatar Sep 10 '20 20:09 bmitchinson

https://www.accountsjs.com/docs/api/express-session/index/#accountsexpress-session

Does using express-session middlewear end up accommodating this? Wrapping this around GraphQL should enable the use of headers / GQL context instead of the JWT on the client side right?

Any advice appreciated, finding my around this lib. thanks!

I've accounts working on nextjs, could help you with it.

I use cookies instead of localstorage, same JWT.

import { AccountsClient } from '@accounts/client';
import { AccountsClientPassword } from '@accounts/client-password';
import GraphQLClient from '@accounts/graphql-client';
import { accountsLink } from '@accounts/apollo-link';
import { ApolloClient, InMemoryCache, HttpLink, from } from '@apollo/client';
import gql from 'graphql-tag';
// import cookies from 'next-cookies';
import { CookieStorage } from 'cookie-storage';

import { useMemo } from 'react'
// import { ApolloClient, InMemoryCache, HttpLink, from } from '@apollo/client';
import { concatPagination } from '@apollo/client/utilities'
// import { accountsClient, apolloClient } from '../utils/accounts'
// import { accountsLink } from '@accounts/apollo-link';


const cookieStorage = new CookieStorage();
// This auth link will inject the token in the headers on every request you make using apollo client
const authLink = accountsLink(() => accountsClient);

const httpLink = new HttpLink({
  uri: 'http://localhost:4000/graphql',
  credentials: 'same-origin',

});

let apolloClient = new ApolloClient({
    ssrMode: typeof window === 'undefined',
		link: from([authLink, httpLink]),
    cache: new InMemoryCache({
      typePolicies: {
        Query: {
          fields: {
            allPosts: concatPagination(),
            myDocuments: concatPagination(),
          },
        },
      },
    }),
  })

const accountsGraphQL = new GraphQLClient({
  graphQLClient: apolloClient,
  userFieldsFragment: gql`
    fragment userFields on User {
      id
      emails {
        address
        verified
      }

      documents {
        id
        title
      }
    }
  `,
});
const accountsClient = new AccountsClient(  {
  // We tell the accounts-js client to use cookieStorage to store the tokens
  tokenStorage: cookieStorage,
}, accountsGraphQL);
const accountsPassword = new AccountsClientPassword(accountsClient);

export { accountsClient, accountsGraphQL, accountsPassword, apolloClient };

function createApolloClient() {
  const httpLink = new HttpLink({
		uri: 'http://localhost:4000/graphql',
		credentials: 'same-origin',
	});
	const authLink = accountsLink(() => accountsClient);
  return new ApolloClient({
    ssrMode: typeof window === 'undefined',
		link: from([authLink, httpLink]),
    cache: new InMemoryCache({
      typePolicies: {
        Query: {
          fields: {
            allPosts: concatPagination(),
            myDocuments: concatPagination(),
          },
        },
      },
    }),
  })
}


export function initializeApollo(initialState = null) {
  const _apolloClient = apolloClient ?? createApolloClient()

  // If your page has Next.js data fetching methods that use Apollo Client, the initial state
  // gets hydrated here
  if (initialState) {
    // Get existing cache, loaded during client side data fetching
    const existingCache = _apolloClient.extract()
    // Restore the cache using the data passed from getStaticProps/getServerSideProps
    // combined with the existing cached data
    _apolloClient.cache.restore({ ...existingCache, ...initialState })
  }
  // For SSG and SSR always create a new Apollo Client
  if (typeof window === 'undefined') return _apolloClient
  // Create the Apollo Client once in the client
  if (!apolloClient) apolloClient = _apolloClient

  return _apolloClient
}

export function useApollo(initialState) {
  const store = useMemo(() => initializeApollo(initialState), [initialState])
  return store
}```
PS: You can also use @accounts/apollo-link to tie it up in the frontend!

agustif avatar Sep 10 '20 21:09 agustif

I've accounts working on nextjs, could help you with it.

I use cookies instead of localstorage, same JWT.

import { AccountsClient } from '@accounts/client';
import { AccountsClientPassword } from '@accounts/client-password';

...

Thank you so much for this! I'll keep you posted. Would love to contribute to official documentation with some of my findings, and that should be possible with your help so, thank you. @agustif

bmitchinson avatar Sep 14 '20 15:09 bmitchinson

@agustif / @pradel any ideas on how could I deliver a token / in the form of a cookie to the client? Instead of being included in a response body. I'm at the point where although this client side configuration is helpful, I need to know how to implement the server to send a cookie.

bmitchinson avatar Sep 16 '20 16:09 bmitchinson

I'd make a new issue except I think the discussions above are relevant to what I'm trying to accomplish. It seems like this issue is to make what I'd like to achieve a first class module of accounts-js

bmitchinson avatar Sep 16 '20 16:09 bmitchinson

I've been doing quite a bit of reading on this and it seems like JWTs in local storage are a big no-no. I know this convo has been going on a while, but it seems like this should be a pretty urgent problem no?

stolinski avatar Jan 08 '21 23:01 stolinski

@agustif / @pradel any ideas on how could I deliver a token / in the form of a cookie to the client? Instead of being included in a response body. I'm at the point where although this client side configuration is helpful, I need to know how to implement the server to send a cookie.

Hey sorry @bmitchinson but I don't know what you mean, can't you read the cookie req.header on getServerSideProps and use accountsClient(validateToken) to decide if render a user auth based view on the server or not?

I would love to help provide more support for accountsjs+nextjs, we maybe could talk over the slack group for Accounts, I've same username there

agustif avatar Jan 10 '21 16:01 agustif

@agustif Hi how are things ? I have tested your configuration with cookie-storage and when using getServerSideProps with await accountsClient.getUser () it does not get me the user, I have no error and when it renders on the client using this same method it gets me the user who is logged in. Do you know why this can be?

edw19 avatar Apr 22 '21 15:04 edw19

@agustif Hi how are things ? I have tested your configuration with cookie-storage and when using getServerSideProps with await accountsClient.getUser () it does not get me the user, I have no error and when it renders on the client using this same method it gets me the user who is logged in. Do you know why this can be?

In serverSideProps you should be able to get the cookie from the ctx.req.headers me thinks! accountsClient doesn't work on the server sadly, so you'd have to read the cookie from the req headers and maybe do something with it like validate it etc? idk moved on without using it further can't help much more Im sorry hope you can get it working!

agustif avatar Apr 22 '21 15:04 agustif

ohh I understand, of course with ctx.req the cookie is read correctly thanks

edw19 avatar Apr 22 '21 18:04 edw19