«[Ошибка GraphQL]: сообщение: для функции запрещено доступ» с использованием JWT в заголовках - PullRequest
1 голос
/ 28 марта 2019

проблема

Запросы, не требующие авторизации, выполняются успешно, но запрос, требующий авторизации JWT, не выполняется.

Ошибка

В консоли браузера появляется следующее сообщение об ошибке:

[GraphQL error]: Message: permission denied for function get_account_info, Location: [object Object], Path: getAccountInfo

И это ошибка, которую я получаю в консоли сервера:

1 error(s) as guest in 101.18ms :: { getAccountInfo { username interface native customNative tutorial email __typename } }

Тот факт, что в сообщении об ошибке указано as guest, означает, что роль не была установлена ​​правильно (в противном случае было бы сказано as loggedin). Я вполне уверен, что эта ошибка не из-за чего-то на стороне SQL, а скорее из-за моего кода JS, но на всякий случай я предоставил ниже некоторый код SQL.

Запрос

Я установил GraphQL Developer Tools и увидел, что именно это было отправлено в запросе:

Запрос

  • URL запроса: http://localhost:3000/graphql
  • Метод: POST
  • HTTP версия: HTTP / 1.1
  • Заголовки:
    • Происхождение: http://localhost:3000
    • Accept-Encoding: gzip, deflate, br
    • Хост: localhost: 3000
    • Accept-Language: pl-PL, pl; q = 0,9, en-US; q = 0,8, en; q = 0,7, fr; q = 0,6, lt; q = 0,5, es; q = 0,4
    • Пользователь-агент: Mozilla / 5.0 (Macintosh; Intel Mac OS X 10_11_3) AppleWebKit / 537.36 (KHTML, как Gecko) Chrome / 73.0.3683.86 Safari / 537.36
    • тип контента: application / json
    • принять: /
    • Рефери: http://localhost:3000/login
    • Cookie: разрешение = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjaWQiOjgsInN1YiI6InN0YXNAbXJzd29yZHNtaXRoLmNvbSIsImlzcyI6Imh0dHA6Ly9td3MtbWxhLmNvbSIsInBlcm1pc3Npb25zIjoxLCJpYXQiOjE1MjIwNzA4NzYsImV4cCI6MTUyMjY3NTY3Nn0.cXoy-SxSc5YVJ36lSmUoKAYU8KpZsZaFOS-xqcmbKPg
    • Соединение: keep-alive
    • Длина содержимого: 179
    • DNT: 1

Обратите внимание, что Cookie имеет авторизацию = [некоторый токен] . Означает ли это, что отсутствует заголовок авторизации , потому что он по какой-то причине живет под Cookie ? Если да, как правильно установить заголовок? Или есть что-то еще, что я делаю неправильно?

Код SQL

Я почти уверен, что с SQL все в порядке, но на всякий случай.

поколение JWT

CREATE FUNCTION private.generate_jwt_for_user(username text)
    RETURNS json_web_token
    LANGUAGE plpgsql
    STABLE
    AS $$
        DECLARE
            n_moderator bigint;
        BEGIN
            SELECT count(*) INTO n_moderator
               FROM private.moderator
               WHERE account = username;

            IF n_moderator = 0
            THEN
                RETURN ('loggedin', username)::json_web_token; -- x::Y means cast x to type Y
            ELSE
                RETURN ('moderator', username)::json_web_token;
            END IF;
        END;
    $$;

get_account_info

CREATE FUNCTION public.get_account_info()
    RETURNS private.account_info
    LANGUAGE SQL
    SECURITY DEFINER
    STABLE
    AS $$
        SELECT *
        FROM private.account_info
        WHERE username = current_setting('jwt.claims.username')
    $$;

Код JavaScript

main.js

// Meteor startup script. Runs reactRoutes, and puts the result in the 'content' div in index.html.

import { Meteor } from 'meteor/meteor'
import { render } from 'react-dom'
import Routes from './routes'
import React from 'react'
import ApolloClient from 'apollo-boost'
import { HttpLink } from 'apollo-link-http'
import { ApolloLink, from } from 'apollo-link'
import { ApolloProvider } from 'react-apollo'

// Connect to the database using Apollo
// Add middleware that adds a Json Web Token (JWT) to the request header

const httpLink = new HttpLink({ uri: '/graphql' });

const authMiddleware = new ApolloLink((operation, forward) => {
  // add the authorization to the headers
  const token = localStorage.getItem('token')
  operation.setContext(({ headers = {} }) => ({
    headers: {
      ...headers,
      authorization: 'Bearer ' + token || null,
    } 
  }));

  return forward(operation);
})

const client = new ApolloClient({
  link: from([
    authMiddleware,
    httpLink
  ]),
});

// <ApolloProvider> allows React to connect to Apollo
// <Routes> allows client-side routing
// The rendered page inserted into the HTML under 'content'
Meteor.startup(() => {
    render(
        <ApolloProvider client={client}>
            <Routes/>
        </ApolloProvider>,
        document.getElementById('content'))
})

app.js

Извинения за длинный код с периодическим комментарием TODO, это все еще в стадии разработки.

import React from 'react'
import jwtDecode from 'jwt-decode'
import { withApollo, graphql } from 'react-apollo'
import gql from 'graphql-tag'
import Nav from './auxiliary/nav'
import Translate from 'react-translate-component'

class UserAppBody extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            activeLanguageId: null
        }
    }

    setLanguage(langId) {
        this.setState({
            activeLanguageId: langId
        })
    }

    render() {
        let native = null
        let username = false
        // TODO: remove all userId references in app
        let tutorial = false
        if (this.props.accountInfo) {
            console.log("jwt: " + localStorage['token'])
            if (this.props.accountInfo.loading) { return <Translate component="div" content="loading.loading" /> }
            console.log(this.props.accountInfo)
            username = this.props.accountInfo.getAccountInfo.username
            tutorial = this.props.accountInfo.getAccountInfo.tutorial
            native =   this.props.accountInfo.getAccountInfo.native
        }

        return (
            <div id="app-container">
                <Nav callbackLogOut={this.props.logOut} username={username} />
                {/* Insert the children according to routes.jsx (this.props.children), along with the childrens' props.
                username should come from query due to being wrapped by graphql for wrapped case; otherwise username is bool: false. */}
                {React.cloneElement(
                    this.props.children, 
                    {
                        username: username,
                        hasSeenTutorial: tutorial,
                        native: native,
                        activeLanguageId: this.state.activeLanguageId, 
                        callbackLanguage: this.setLanguage.bind(this),
                        callbackUser: this.props.setUser,
                        callbackLogOut: this.props.logOut
                    }
                )}
            </div>
        )
    }
}

// UserAppBody will be wrapped in AppBody if user is logged in, this setup comes before the wrapping
// Calling graphql on this turns it into a function which returns a React element (needed below)
const accountInfoQuery = gql`query{
    getAccountInfo {
        username
        interface
        native
        customNative
        tutorial
        email
    }
}`

const accountInfoQueryConfig = {
    name: 'accountInfo'
}
const SignedInAppBody = graphql(accountInfoQuery, accountInfoQueryConfig)(UserAppBody)


class AppBody extends React.Component {
    constructor(props) {
        super(props)
        const raw_jwt = localStorage.getItem('token')
        this.state = {
            isLoggedIn: !!raw_jwt // true if there is a jwt in local storage, false otherwise
        }
    }

    setUser(raw_jwt) {
        const jwt = jwtDecode(raw_jwt)

        // Check if the token has expired
        // Note that getTime() is in milliseconds, but jwt.exp is in seconds
        const timestamp = (new Date).getTime()
        if (!!jwt && timestamp < jwt.exp * 1000) {
            // If the token is still valid:
            // Store the token in memory, to be added to request headers
            localStorage.setItem('token', raw_jwt)
            // Set the state, to change the app
            this.setState({
                isLoggedIn: true
            })
            // Automatically refresh the token
            this.refreshTimer = setInterval(this.refresh, 1000*60*20)  // Refresh every 20 minutes
            console.log('timer set up')

        } else {
            // If the token is no longer valid, log out to clear information
            this.logOut()
        }
    }

    logOut() {
        // Clear everything from setUser (state, memory, refreshing)
        localStorage.removeItem('refreshToken')
        localStorage.removeItem('token')
        clearInterval(this.refreshTimer)
        console.log('logging out')
        // second argument is a callback that setState will call when it is finished
        this.setState( { isLoggedIn: false }, this.props.client.resetStore() )
    }

    refresh() {
        // Get a new token using the refresh code
        this.props.refresh({variables: {input: {refreshToken: localStorage.getItem('refreshToken')}}})
        .then((response) => {
            // Store the new token
            const raw_jwt = response.data.refresh.jsonWebToken
            localStorage.setItem('token', raw_jwt)
        }).catch((error) => {
            // If we can't connect to the server, try again
            if (error.networkError) {
                console.log('network error?') //TODO
                //this.refresh()
            } else { //TODO
                // If we connected to the server and refreshing failed, log out
                console.log('error, logging out')
                console.log(error)
                this.logOut()
            }
        })
    }

    componentWillMount() {
        const raw_jwt = localStorage.getItem('token')
        if (!!raw_jwt) {
            console.log('found json web token, running setUser as App compenent mounts')
            this.setUser(raw_jwt)
            this.refresh()
        }
    }

    render() {
        let AppBodyClass

        if (this.state.isLoggedIn) {
            AppBodyClass = SignedInAppBody
        } else {
            AppBodyClass = UserAppBody
        }
        return <AppBodyClass children={this.props.children} setUser={this.setUser.bind(this)} logOut={this.logOut.bind(this)} />
    }
}

const refresh = gql`mutation($input:RefreshInput!) {
    refresh(input:$input) {
        jsonWebToken
    }
}`
const refreshConfig = {
    name: 'refresh'
}

export default withApollo(graphql(refresh, refreshConfig)(AppBody))

Ответы [ 2 ]

2 голосов
/ 28 марта 2019

Обратите внимание, что Cookie имеет авторизацию = [некоторый токен]. Означает ли это, что заголовок авторизации отсутствует, потому что он по какой-то причине живет под Cookie? Если да, то как правильно установить заголовок? Или есть что-то еще, что я делаю не так?

Это странно, но код вашего клиента кажется правильным; попробуйте использовать другой инструмент разработки, чтобы увидеть, что на самом деле отправляется. Слово «Носитель» тоже было отброшено, очень странно.

Тот факт, что в сообщении об ошибке говорится как гость, означает, что роль не была установлена ​​правильно (в противном случае она будет указана как loggedin). Я вполне уверен, что эта ошибка не из-за чего-то на стороне SQL, а скорее из-за моего кода JS, но на всякий случай я предоставил ниже некоторый код SQL.

Поместив токен JWT в инструмент jwt.io, я вижу, что тело токена:

{
  "cid": 8,
  "sub": "s[AN EMAIL ADDRESS]m",
  "iss": "http://mws-mla.com",
  "permissions": 1,
  "iat": 1522070876,
  "exp": 1522675676
}

В нем нет утверждения о роли, поэтому PostGraphile не будет пытаться менять роли. Однако это не соответствует JWT, который вы генерируете в PostgreSQL, поэтому я подозреваю, что этот файл cookie вводит в заблуждение. Я считаю, что вы вообще не отправляете заголовок авторизации.

Попробуйте отладить ваше промежуточное ПО аутентификации:

const authMiddleware = new ApolloLink((operation, forward) => {
  // add the authorization to the headers
  const token = localStorage.getItem('token')
  operation.setContext(context => {
    const ctx = {
      ...context,
      headers: {
        ...context.headers,
        authorization: 'Bearer ' + token || null,
      } 
    };
    console.log(ctx);
    return ctx;
  });

  return forward(operation);
})

(Примечание: ранее вы только держали заголовки в контексте, в приведенном выше коде я теперь также пропускаю и другие свойства.)

1 голос
/ 01 апреля 2019

@ Бенджи прав, что промежуточное ПО не работает и поэтому заголовок не добавляется. Проблема в том, что apollo-boost не допускает опцию link. ApolloClient следует импортировать из apollo-client.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...