OrchardCMS / OrchardCore

Orchard Core is an open-source modular and multi-tenant application framework built with ASP.NET Core, and a content management system (CMS) built on top of that framework.
https://orchardcore.net
BSD 3-Clause "New" or "Revised" License
7.36k stars 2.37k forks source link

Fetching Data from a GraphQL API to create a website #6280

Closed mountaingeru closed 4 months ago

mountaingeru commented 4 years ago

Hi,

Before telling you about my issue, I want to show my gratitude and appreciation for this product, which I have recently discovered, and what I have been able to see really pleased very much. Thank you very much!!!

And now the issue.

I am trying to create web pages from content obtained through requests to the GraphQL API. The CMS project is published on an IIS Server and I make successful requests to the API from Postman. However, when I try to fetch the API from JavaScript (hosted in the same server) I cannot get the content.

I have made serveral tests changing the JavaScript code with no success:

        const url = "./cms/api/graphql";

        const opts = {
            method: 'POST',
            mode: 'no-cors',
            credentials: 'include',
            //credentials: 'omit',
            //headers: { 'Content-Type': 'application/json' },
            headers: { 'Content-Type': 'application/graphql' },
            body: JSON.stringify({"query": myQuery})
            //body: JSON.stringify({ query: myQuery })
            //body: myQuery
        };

        //require('isomorphic-fetch');

        fetch(url, opts)
            .then(function (response) {
                return response.json();
            })
            .then(function (data) {
                console.log(JSON.parse(data));
            })
            .catch(function (error) {
                console.error('Unable to query api/graphql.', error);
            });

The browser console shows this:

Failed to load resource: the server responded with a status of 400 (Bad Request) SyntaxError: Unexpected end of JSON input

Going to the Event Viewer the request to the CMS raises the following error:

Category: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware EventId: 1 RequestId: 8000aa2b-0000-eb00-b63f-84710c7967bb RequestPath: /sala-prensa/cms/api/graphql SpanId: |690af6cf-4c3e9963535f227e. TraceId: 690af6cf-4c3e9963535f227e ParentId:

An unhandled exception has occurred while executing the request.

Exception: System.NullReferenceException: Object reference not set to an instance of an object. at OrchardCore.Apis.GraphQL.GraphQLMiddleware.ExecuteAsync(HttpContext context, ISchemaFactory schemaService) in C:\projects\orchardcore\src\OrchardCore.Modules\OrchardCore.Apis.GraphQL\GraphQLMiddleware.cs:line 146 at OrchardCore.Apis.GraphQL.GraphQLMiddleware.Invoke(HttpContext context, IAuthorizationService authorizationService, IAuthenticationService authenticationService, ISchemaFactory schemaService) in C:\projects\orchardcore\src\OrchardCore.Modules\OrchardCore.Apis.GraphQL\GraphQLMiddleware.cs:line 63 at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) at OrchardCore.Diagnostics.DiagnosticsStartupFilter.<>c__DisplayClass3_0.<b1>d.MoveNext() in C:\projects\orchardcore\src\OrchardCore.Modules\OrchardCore.Diagnostics\DiagnosticsStartupFilter.cs:line 36 --- End of stack trace from previous location where exception was thrown --- at Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.gAwaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)

Any help would be appreciated. Best regards.

sebastienros commented 4 years ago

You are sending a POST request which will require an antiforgery token. Another option with graphql is to use GET requests, which is documented here: https://docs.orchardcore.net/en/dev/docs/reference/modules/Apis.GraphQL/#get-request

mountaingeru commented 4 years ago

Thank you very much for your answer and I am sorry I was late in answering.

I have create a MVC project and included the following code in ConfigureServices

services.AddAuthentication(options =>
            {
                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
            })
            .AddCookie()
            .AddOpenIdConnect(options =>
            {
                options.Authority = "https://localhost:44396/connect/token";
                options.ClientId = "e0f660a2cf2a47babac40a4a8c24e7e0";
                options.ClientSecret = "76945d3917a4456db5a41fc2949d6439";
                options.ResponseType = "code id_token";
                options.RequireHttpsMetadata = false;
                options.GetClaimsFromUserInfoEndpoint = true;
            });

I include the js code to fetch the GraphQL API using a POST request. I tried several combinations

        const opts = {
            method: "POST",
            mode: 'no-cors',
            credentials: 'include',
            //headers: { "Content-Type": "application/json" },
            headers: { "Content-Type": "application/graphql" },
            //body: JSON.stringify({query: myQuery1})
            body: JSON.stringify({ query: myQuery1 })
            //body: JSON.stringify({ "query": myQuery1 })
            //body: myQuery1
        };. 

In the developer tools of the brower I see three cookies related to authoritation: orchauth_Default, orchantiforgery_Default and .AspNetCore.Antiforgery.QrPYhbOUQ0o.

The request fetched seems to include the authoritation cookies information:

General Request URL: https://localhost:44396/api/graphql Request Method: POST Status Code: 500 Internal Server Error Remote Address: [::1]:44396 Referrer Policy: no-referrer-when-downgrade

Response Headers Content-Length: 4962 Content-Type: text/plain Date: Sat, 30 May 2020 14:33:42 GMT Server: Microsoft-IIS/10.0 X-Powered-By: ASP.NET

Request Headers Accept: / Accept-Encoding: gzip, deflate, br Accept-Language: en,es-ES;q=0.9,es;q=0.8 Cache-Control: no-cache Connection: keep-alive Content-Length: 396 Content-Type: text/plain;charset=UTF-8 Cookie: _ga=GA1.1.625725641.1589660747; .AspNetCore.Antiforgery.QrPYhbOUQ0o=CfDJ8GAc_xY9fJ...; orchantiforgery_Default=CfDJ8Nx_bbMNqvtLnrArzxb7Ojys...; orchauth_Default=CfDJ8Nx_bbMNqvtLnrArzxb7Oj... Host: localhost:44396 Origin: https://localhost:44326 Pragma: no-cache Referer: https://localhost:44326/ Sec-Fetch-Dest: empty Sec-Fetch-Mode: no-cors Sec-Fetch-Site: same-site User-Agent: Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36

And the response is:

System.NullReferenceException: Object reference not set to an instance of an object. at OrchardCore.Apis.GraphQL.GraphQLMiddleware.ExecuteAsync(HttpContext context, ISchemaFactory schemaService) in C:\projects\orchardcore\src\OrchardCore.Modules\OrchardCore.Apis.GraphQL\GraphQLMiddleware.cs:line 146 at OrchardCore.Apis.GraphQL.GraphQLMiddleware.Invoke(HttpContext context, IAuthorizationService authorizationService, IAuthenticationService authenticationService, ISchemaFactory schemaService) in C:\projects\orchardcore\src\OrchardCore.Modules\OrchardCore.Apis.GraphQL\GraphQLMiddleware.cs:line 70 at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) at OrchardCore.Diagnostics.DiagnosticsStartupFilter.<>c__DisplayClass3_0.<b__1>d.MoveNext() in C:\projects\orchardcore\src\OrchardCore.Modules\OrchardCore.Diagnostics\DiagnosticsStartupFilter.cs:line 47 --- End of stack trace from previous location where exception was thrown --- at Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.Invoke(HttpContext context) at OrchardCore.Modules.ModularTenantRouterMiddleware.Invoke(HttpContext httpContext) in C:\projects\orchardcore\src\OrchardCore\OrchardCore\Modules\ModularTenantRouterMiddleware.cs:line 83 at OrchardCore.Environment.Shell.Scope.ShellScope.UsingAsync(Func`2 execute) in C:\projects\orchardcore\src\OrchardCore\OrchardCore.Abstractions\Shell\Scope\ShellScope.cs:line 103 at OrchardCore.Modules.ModularTenantContainerMiddleware.Invoke(HttpContext httpContext) in C:\projects\orchardcore\src\OrchardCore\OrchardCore\Modules\ModularTenantContainerMiddleware.cs:line 61 at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

HEADERS

Accept: / Accept-Encoding: gzip, deflate, br Accept-Language: en,es-ES;q=0.9,es;q=0.8 Cache-Control: no-cache Connection: keep-alive Content-Length: 396 Content-Type: text/plain;charset=UTF-8 Cookie: _ga=GA1.1.625725641.1589660747; .AspNetCore.Antiforgery.QrPYhbOUQ0o=CfDJ8GAc_xY9fJ...; orchantiforgery_Default=CfDJ8Nx_bbMNqvtLnrArzxb7Ojys...; orchauth_Default=CfDJ8Nx_bbMNqvtLnrArzxb7Oj... Host: localhost:44396 Pragma: no-cache Referer: https://localhost:44326/ User-Agent: Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36 Origin: https://localhost:44326 Sec-Fetch-Site: same-site Sec-Fetch-Mode: no-cors Sec-Fetch-Dest: empty

However, making the request from Postman (including the Bearer token I get by requesting https://localhost:44396/connect/token) everything goes fine.

I also tried with the GET request. Everything goes fine with Postman but does not work when fetching from JavaScript.

Any idea about what I am doing wrong? I am kind of stuck

I am sorry if the question is silly, but I am new at these things. Thanks in advance!

sebastienros commented 4 years ago

Is your website the same one that is running the graphql service? You need to be authenticated to do the queries. Unless you allow "Anonymous" to run the queries. I believe that in the case of Postman there is a valid auth cookie, but not as the user of your app.

sebastienros commented 4 years ago

We might want to create a guide showing how to configure an implement openid such that we can use apis from a web app.

mountaingeru commented 4 years ago

The graphql service and the website are different web apps. We tried hosting them in the same server and in different servers.

After many attempts and thanks to the collaboration of a colleague we achieved a satisfactory solution. We don't solve everything in javascript, but the way it works is fine for us. Here is what we have done (assuming we included things that could be done better):

  1. We followed this guide (https://medium.com/swlh/orchard-core-open-id-connect-and-graphql-f003a222260a) to configure OpenId.

  2. We create our Net Core MVC app.

  3. We include a service to get the token used in the GraphQL request.

public class TokenService : ITokenService
    {
        private readonly IHttpClientFactory _clientFactory;
        private readonly IOptions<OrchardConfig> _config;

        public TokenService(IOptions<OrchardConfig> config)
        {
            _config = config;
        }

        public async Task<string> GetTokenAsync()
        {
            IList<KeyValuePair<string, string>> nameValueCollection = new List<KeyValuePair<string, string>> {
                { new KeyValuePair<string, string>("client_id", _config.Value.ClientId) },
                { new KeyValuePair<string, string>("client_secret", _config.Value.ClientSecret) },
                { new KeyValuePair<string, string>("grant_type", _config.Value.GrantType) }
            };

            var client = new HttpClient();
            var response = await client.PostAsync(_config.Value.UrlToken, new FormUrlEncodedContent(nameValueCollection));

            var tokenResponse = await response.Content.ReadAsStringAsync();

            return tokenResponse;
        }
    }

Where the UrlToken (url of the service to get the token /connect/token ), ClientId, ClientSecret and GrantType (="client_credentials")) are stored, for the moment, in a created section called "OrchardConfig" in the appsettings.json

  1. We include the service in the pipeline:

services.AddScoped<ITokenService, TokenService>();

  1. We inject the service to our controller and the token:
        public async Task<IActionResult> Index()
        {
            var token = JsonConvert.DeserializeObject<BearerToken>(await _tokenService.GetTokenAsync());
            ViewBag.Token = $"{token.token_type} {token.access_token}";
            ViewBag.UrlApi = _config.Value.UrlApi;
            return View();
        }

where BeareToken is a class we have created. We include in the ViewBag the toke and URL of the GraphQL service (also in the appsettings.json)

    public class BearerToken
    {
        public string token_type { get; set; }
        public string access_token { get; set; }
        public int expires_in { get; set; }
    }
  1. In our view we get the values from the ViewBag:
    <script>
        var urlApi = "@ViewBag.UrlApi";
        var myToken = "@ViewBag.Token";
   </script>
  1. Finally we make the fetch request to the GraphQL service:
 const myQuery1 = `{
                                 paginaNotasPrensa {
                                   bag {
                                     contentItems {
                                        bla bla bla
                                     }
                                   }
                                 }
                               }`;

        const url = urlApi;

        var myHeaders = new Headers();
        myHeaders.append("Content-Type", "application/json");
        myHeaders.append("Authorization", myToken);

        var requestOptions = {
            method: 'POST',
            headers: myHeaders,
            body: JSON.stringify({ query: myQuery1 })
        };

        fetch(url, requestOptions)
        .then(function (response) {
            return response.json();
        })
     .then(function (res) {
            salaPrensa.drawNotasPrensa(res.data);
        })
        .catch(function (error) {
            console.error('Unable to query api/graphql (POST).', error);
        });