gatsbyjs / gatsby

The best React-based framework with performance, scalability and security built in.
https://www.gatsbyjs.com
MIT License
55.27k stars 10.31k forks source link

apple-app-site-association #13437

Closed jeffchuber closed 5 years ago

jeffchuber commented 5 years ago

I'm interested in strategies to add apple-app-site-association to a Gatsby site. more here

Apple requires that it be at https://domain.com/.well-known/apple-app-site-association and return flat json. (no html!)

In m production website, I'm using Cloudfront in front of s3, so linking directly to it does not work. The Gatsby router picks up the route and gives me a 404.

Thank you!

DSchau commented 5 years ago

@jeffchuber wouldn't you need to add this to the static folder? https://www.gatsbyjs.org/docs/static-folder/

This will be copied as-is to the public folder, which can then be deployed to e.g. S3 very easily.

Also: this seems like this could be a good idea for a plugin to create this automatically!

Going to close as answered, but please feel free to re-open/reply if we can help further!

jeffchuber commented 5 years ago

@DSchau thanks for the quick reply!

The issue is not getting a file into the static folder, the issue is accessing that file by the route/url apple demands https://domain.com/.well-known/apple-app-site-association.

Desired scenario:

  1. Apple's crawler visits http://localhost:8000/.well-known/apple-app-site-association
  2. It is presented with raw json. eg:
    {
    "webcredentials": {
    "apps": [
      "KAW43335BG.com.companyName.App1",
      "KAW43335BG.com.companyName.App2"
    ]
    }
    }
jeffchuber commented 5 years ago

Looks like this is not an uncommon issue: https://github.com/gatsbyjs/gatsby/issues/4144

DSchau commented 5 years ago

@jeffchuber you need to set up a redirect for your provider. For example, with S3: https://docs.aws.amazon.com/AmazonS3/latest/dev/how-to-page-redirect.html

I've set up an example with Netlify with this repo, and you can see it working:

https://gatsby-apple-app-site-association.netlify.com/.well-known/apple-app-site-association

jeffchuber commented 5 years ago

@DSchau I really appreciate the reply! I do think this is something a lot of devs will encounter or I wouldn't spend the time.

Unfortunately it needs to be solved at the routing level because Apple rejects redirects:

Screen Shot 2019-04-19 at 11 11 09 AM

https://developer.apple.com/documentation/security/password_autofill/setting_up_an_app_s_associated_domains

My understanding is that enabling a dev to add a route to gatsby-node.js that can point directly at a file (instead of a React component) would be ideal. Something like this perhaps.

createPage({
  path: '/.well-known/apple-app-site-association',
  file: path.resolve('./public/.well-known/apple-app-site-association.json'),
})
DSchau commented 5 years ago

Ah! Come on Apple, not making it easy on us!

Let's re-open this and find a more robust solution.

jeffchuber commented 5 years ago

@DSchau what do you think about the idea above of setting up a route to serve a static file?

rafcontreras commented 5 years ago

Hi @jeffchuber,

Why do you need to route to the apple-app-site-association file? Safari and your iOS app look for if the file exists on your server for credential sharing.

It may be that your server is restricting access to dotfiles which are hidden to some OSs.

I use express to serve the gatsby files and express.static to allow access to the .well-known folder.

app.use(
  express.static(serverSettings.root, {
    dotfiles: "allow"
  })
);

app.get("/.well-known/change-password", (req, res) => {
  res.redirect(301, "/change-password");
});
Screen Shot 2019-04-26 at 6 03 22 AM
jeffchuber commented 5 years ago

@rafcontreras thanks for the idea!

currently im using Cloudfront -> S3 -> gatsby static site.

I found a workaround which is hosting on a subdomain, so I'm going to close this for now out of respect for the community.

adelin-b commented 5 years ago

Hi @jeffchuber,

Why do you need to route to the apple-app-site-association file? Safari and your iOS app look for if the file exists on your server for credential sharing.

It may be that your server is restricting access to dotfiles which are hidden to some OSs.

I use express to serve the gatsby files and express.static to allow access to the .well-known folder.

app.use(
  express.static(serverSettings.root, {
    dotfiles: "allow"
  })
);

app.get("/.well-known/change-password", (req, res) => {
  res.redirect(301, "/change-password");
});
Screen Shot 2019-04-26 at 6 03 22 AM

Where do you use express exactly ? here are the 2 ressource I found to serve this .well-know folder https://www.gatsbyjs.org/docs/api-proxy/ https://www.gatsbyjs.org/packages/gatsby-plugin-express/

second one also dont give info of where the express app should be called

rafcontreras commented 5 years ago

Hey @adberard

I used Gatsby to create a hybrid app, so I need a server in between the user and the different APIs as some have dumb CORS settings.

I'm using expressJS to proxy API requests and I'm hosting it in Azure.

Here's my very small express server server.js:

const url = require("url");
const express = require("express");
const gatsyExpress = require("gatsby-plugin-express");
const proxy = require("express-http-proxy");
const bodyParser = require("body-parser");
const expressStaticGzip = require("express-static-gzip");

// NodeJS Express server
const app = express();

// In case the server is behind a proxy
app.enable("trust proxy");

/* Removes the X-Powered-By header
to make it slightly harder for attackers
to see what potentially-vulnerable
technology powers your site*/
app.disable("x-powered-by");

// Parse API queries
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

// Redirect .well-known requests .well-known/change-password
app.get("/.well-known/change-password", (request, response) => {
  response.redirect(301, "/change-password");
});

// Proxy API
app.use(
  "/api",
  proxy("https://api.domain.tld", {
    https: true,
    proxyReqPathResolver(request) {
      const reqPath = url.parse(request.url).path;
      return [api, reqPath].join("/");
    },
    proxyErrorHandler(error, next) {
      console.log(error)
      next(error);
    }
  })
);

// Send pre-compressed static files
app.use(
  "/",
  expressStaticGzip("/public", {
    enableBrotli: true,
    orderPreference: ["br", "gz"]
  })
);

// Routes
app.use(
  gatsyExpress("gatsby-express.json", {
    publicDir: "/public",
    template: "public/404/index.html",
    redirectSlashes: true
  })
);

// Allow dotfiles
app.use(
  express.static(serverSettings.root, {
    dotfiles: "allow"
  })
);

// Serve 404 page on 404
app.use((req, res, next) => {
  const err = new Error("Not Found");
  err.status = 404;
  next(err);
});

app.use((err, req, res, next) => {
  res.status(err.status || 500);
  res.sendFile("404/index.html", {
    root: "/public"
  });
});

// HTTP server
app
  .listen(3000, () => {})
  .on("error", e => {
    console.log(e);
  });

And here is my web.config in case someone need it

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <urlCompression doStaticCompression="false" doDynamicCompression="true" />
    <httpProtocol>
      <customHeaders>
        <remove name="X-Powered-By" />
        <add name="Access-Control-Allow-Origin" value="*" />
        <add name="X-Frame-Options" value="DENY" />
        <add name="Cache-Control" value="public, max-age=0, must-revalidate" />
        <add name="X-XSS-Protection" value="1; mode=block" />
        <add name="X-Content-Type-Options" value="nosniff" />
        <add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains; preload" />
        <add name="Referrer-Policy" value="no-referrer" />
        <add name="Content-Security-Policy" value="default-src 'self' https://*.google-analytics.com https://*.google.com; img-src * blob: data:; media-src *; object-src *; script-src * 'unsafe-inline' 'unsafe-eval'; style-src * 'unsafe-inline';" />
        <add name="X-Permitted-Cross-Domain-Policies" value="none" />
        <add name="Feature-Policy" value="accelerometer 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; payment 'none'; usb 'none'" />
        <add name="X-DNS-Prefetch-Control" value="on"/>
      </customHeaders>
    </httpProtocol>
    <staticContent>
      <clientCache cacheControlMode="DisableCache" />
      <mimeMap fileExtension=".svg" mimeType="image/svg+xml" />
      <mimeMap fileExtension=".json" mimeType="application/json" />
      <mimeMap fileExtension=".webmanifest" mimeType="application/manifest+json" />
      <mimeMap fileExtension=".*" mimeType="text/plain" />
      <mimeMap fileExtension=".mp4" mimeType="video/mp4" />
      <mimeMap fileExtension=".m4v" mimeType="video/m4v" />
      <mimeMap fileExtension=".aac" mimeType="audio/aac" />
      <mimeMap fileExtension=".oga" mimeType="audio/ogg" />
      <mimeMap fileExtension=".webm" mimeType="video/webm" />
      <mimeMap fileExtension=".ogv" mimeType="video/ogv" />
      <mimeMap fileExtension=".ogg" mimeType="video/ogg" />
      <mimeMap fileExtension=".m4a" mimeType="video/mp4" />
    </staticContent>
    <modules runAllManagedModulesForAllRequests="false" />
    <iisnode watchedFiles="web.config;*.js;"/>
    <handlers>
      <add name="iisnode" path="serve.js" verb="*" modules="iisnode" />
    </handlers>
    <security>
      <requestFiltering removeServerHeader="true">
        <hiddenSegments>
          <remove segment="bin" />
        </hiddenSegments>
      </requestFiltering>
    </security>
    <rewrite>
      <rules>
        <clear />
        <rule name="NodeInspector" patternSyntax="ECMAScript" stopProcessing="true">
          <match url="^serve.js\/debug[\/]?" />
        </rule>
        <rule name="app" enabled="true" patternSyntax="ECMAScript" stopProcessing="true">
          <match url="iisnode.+" negate="true" />
          <conditions logicalGrouping="MatchAll" trackAllCaptures="false" />
          <action type="Rewrite" url="serve.js" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>
PolGuixe commented 4 years ago

Has anyone found a solid approach for this?

Is creating the file on postBuild a good solution?

Svarto commented 3 years ago

I also need a solution to this... @jeffchuber how did you host it on a subdomain? Does your links then all have to be written as subdomain.example.com or is it possible to still have the universal link applicable to example.com?

@PolGuixe did you find a good solution?

whitedogg13 commented 3 years ago

Hi all, I have successfully linked my app with a gatsby-js powered website recently. It's not complex, and you don't actually need to use any server-rendered route, just some little gotchas.

My website is hosted on Netlify, but I believe the same idea applies to all hosting platforms.

Here're the steps.

  1. Put your apple-app-site-association in static folder, as mentioned by @DSchau
    {"applinks":{"apps":[],"details":[{"appID":"HE7MCCDHL4.com.revteltech.nfcopenrewriter","paths":["*"]}]}}

    Please notice the apps prop should be an empty array. (Apple has some updates regarding this format, but the above snippet works correctly for me)

By Apple's doc, you can serve it under / or /.well-known, we use the former one here.

  1. serve this file with correct content-type, it should be application/json. More info check official Apple doc. For netlify users, you can put a _headers file in your static directory, with following content:
    /apple-app-site-association
    Content-Type: application/json

By default netlify serve it as plain/text, and I guess that's the reason why some people cannot make it work

  1. Verify it.

You can use official tool. After you run the program, check the Link to Application field.

The following screenshot means your association file is NOT recognized by Apple, you should check the format or content-type mentioned above:

image

On the other hand, the following screenshot means the association file is correctly served. Apple might need some time to process the link between your app and website. If there's still an issue, then it should be in your iOS app rather than your website. (in my case, the validator still show the same kind of messages but the universal link already works correctly):

image

Finally, open your website in iOS safari browser, see if it hints you about your app on the top area:

image

Hope that helps!