Body: <p>I am trying to create a download link to an ASSET in craft that I can send via email - the client clicks the email link (which has a token query string attached to validate the link) and is presented with a Craft page to download the attachment.</p>
  <p><strong>But</strong> I keep getting a 404 error, saying invalid route.</p>
  <ul>
  <li>Im trying to create a token - which will only last for say 24H.</li>
  </ul>
  <p><code>{% set token = craft.app.tokens.createToken() %}</code></p>
  <p>I am aware that you need to pass in arguments to createToken() but I am not sure what they are as the docs are somewhat highlevel:</p>
  <p><a href="https://docs.craftcms.com/api/v4/craft-services-tokens.html#public-methods" rel="nofollow noreferrer">https://docs.craftcms.com/api/v4/craft-services-tokens.html#public-methods</a></p>
  <ul>
  <li><p>Store the token in Craft <em>(haven't worked this part out yet)</em></p>
  </li>
  <li><p>Send a link, with the token as part of a query string</p>
  </li>
  </ul>
  <p><code>&lt;a href=&quot;&lt;PAGE_URL&gt;?token={{ token }}&quot;&gt;Download here&lt;/a&gt;</code></p>
  <ul>
  <li><p>When the user visits the page, the twig template <strong>1) gets the query string:</strong></p>
  <p>{% set token_from_queryString = craft.app.request.getQueryParams('token')|first %}</p>
  </li>
  <li>
  <ol start="2">
  <li>Validates that the query string is the same as the one stored and so presents the download link to the user:</li>
  </ol>
  </li>
  </ul>
    
      
        <h2>LOGIC TEST</h2>
  <pre><code>  &lt;div&gt;
      {% if token == token_from_queryString %}
        &lt;p&gt;Token matches - download file:&lt;/p&gt;
      {% else %}
        &lt;p&gt;Seems that the link used is no longer acspected, please create a new one:&lt;/p&gt;
      {% endif %}
    &lt;/div&gt;  
  &lt;/div&gt;
  </code></pre>
    
  <p>Issues:</p>
  <ol>
  <li>I don't know if this is actually viable using this option.</li>
  <li>if I do not include the query string in the URL i.e. do not add ?token=(token value or a fake value) - then the page works as exspected. But if I add the query string I get given a 404 route error.
  How am I supposed to be formatting the tokens route, is this were the token lives? or a route to be directed to via the link. I haven't found any Craft docs on this as of yet.</li>
  </ol>
  <p>-- Update of work:</p>
  <p>I have got most of this code to work, but still am having issues with the route of the TOKEN and hiding the original URL of the ASSET.</p>
  <p>ISSUES:</p>
  <p>A) Does Craft automatically store tokens, if so where? I saw reference on discord to storing them in the DB, but no docs to back this up yet.</p>
  <p>B) I set the route to be the file URL in hopes it could be used in a <code>&lt;a href=&quot;{{ tokenA }}&quot; download&gt;{{ download.title }}&lt;/a&gt;</code>, but this says that the  file cannot be found. But if I use &quot;getToken&quot; variable below, that uses <code>getTokenRoute()</code>, then it spits out the original URL of the download. So the token route is set to the URL (maybe I could be misunderstanding), but the token cannot be used to route to a download path.</p>
  <p>This is all with the goal of validating the user via a token and hiding the URL of the original download file.</p>
  <p>URL needing to be used:</p>
  <pre><code>https://&lt;your site address&gt;/&lt;page with this code on it&gt;?tokenA=&lt;the token number produced by tokenA (also printed to the page at TOKEN CODE:&gt;
  </code></pre>
  <p>Code below</p>
  <pre><code>&lt;main&gt;
  &lt;h2&gt;TOKENs&lt;/h2&gt;
  
  &lt;div class=&quot;wrapper&quot; style=&quot;background:lightblue&quot;&gt;
    {# Get file #}
    {% set download = craft.assets().fileName('&lt;NAME OF FILE TO BE USED AS ONE TIME DOWNLOAD&gt;').one() %}
  
    {# Create token - ISSUE A) B) #}
    {% set tokenA = craft.app.tokens.createToken([download.url]) %}
  
    {# Gets the Token from the query string #}
    {% set queryString = craft.app.request.getQueryParams('tokenA')|first %}
  
    {# Takes the original token path - and gets it #}
    {# R) download path of the asset - download #}
    {% set getToken = craft.app.tokens.getTokenRoute(queryString)|first %}
  
    &lt;p&gt;TOKEN CODE: {{ tokenA }}&lt;/p&gt;
    &lt;a href=&quot;{{ tokenA }}&quot;&gt;Download&lt;/a&gt;
    &lt;p&gt;Result of get token: {{ getToken }}&lt;/p&gt;
  &lt;/div&gt;
  
  &lt;div class=&quot;wrapper&quot;&gt;
    &lt;h2&gt;Params&lt;/h2&gt;
    &lt;div class=&quot;wrapper&quot; style=&quot;background:lightskyblue&quot;&gt;
      &lt;h1&gt;{{ getToken }}&lt;/h1&gt;
  
      {# If getToken has length i.e. it has been found #}
      {% if getToken|length %}
        &lt;h1&gt;Working&lt;/h1&gt;
        {# &lt;a href=&quot;{{ getToken }}&quot;&gt;link&lt;/a&gt; #}
      {% else %}
        &lt;h1&gt;Broken&lt;/h1&gt;
      {% endif %}
  
        {# &lt;p&gt;getQueryString(): {{ param }}&lt;/p&gt;
        &lt;p&gt;getQueryParams(): &lt;br&gt; 
          {% for block in param2 %}
            {{ block }}&lt;br&gt;
          {% endfor %}
        &lt;/p&gt;
        &lt;p&gt;&lt;strong&gt;getQueryParams('token')|first: {{ param4 }}&lt;/strong&gt; &lt;&lt;&lt; WINNER&lt;/p&gt;
        &lt;p&gt;getQueryStringWithoutPath(): {{ param3 }}&lt;/p&gt; #}
      
    &lt;/div&gt;
  &lt;/div&gt;
  
  
  
  &lt;div class=&quot;wrapper&quot;&gt;
    &lt;h2&gt;LOGIC TEST&lt;/h2&gt;
  
    &lt;div class=&quot;wrapper&quot; style=&quot;background:lightyellow&quot;&gt;
      {% if getToken|length %}
        {# If token is valid allow the user to download the below - #}
        {# ISSUE: need to hide the original url of the download so it cannot be shared - #}
        &lt;a href=&quot;{{ download.url }}&quot; download&gt;{{ download.title }}&lt;/a&gt;
  
        {# Delete the used token from the system (this doens't seem to be working) #}
        {{ craft.app.token.deleteTokenById(getToken) }}
      {% else %}
        &lt;p&gt;Seems that the link used is no longer acspected, please create a new one:&lt;/p&gt;
      {% endif %}
    &lt;/div&gt;  
  &lt;/div&gt;
  </code></pre>
  
  
Comments.ContentLicense: CC BY-SA 4.0
Comments.CreationDate: 2022-09-21T17:26:29.137Z
Comments.Text: This _seems_ like it’d be a viable approach, where you’d want to store the token’s ID after creating it. That way, you could retrieve its value with `getTokenById()`. For issue #2, have you tried `&token=[value]` instead of `?token=[value]`? Don’t know offhand how the route should be formatted as an argument, but you’ll probably find examples in Craft’s source.
Comments.UserDisplayName: Matt Stein
Comments.ContentLicense: CC BY-SA 4.0
Comments.CreationDate: 2022-09-22T08:16:26.850Z
Comments.Text: Hi Matt, thanks for the help - turns out there is very little mention of createToken in the Craft source files - which has also been mentioned on the discord. Adding the & instead of the ? does work, but I think it now believe the path should include the token as part of the sites address and not a query string. I'll update the question with an example.
Comments.UserDisplayName: Wally
Comments.ContentLicense: CC BY-SA 4.0
Comments.CreationDate: 2022-09-22T17:19:02.970Z
Comments.Text: To be fair, you’re working with something Craft uses more internally. The good news is there are examples in the source to follow, like this: https://github.com/craftcms/cms/blob/f45c185f4aa0d164f40fbdb34061d0150256ef1c/src/console/controllers/UsersController.php#L414-L419
  
  You could mimic that pattern, including the use of that `UrlHelper::urlWithToken()` method to build the URL.
Comments.UserDisplayName: Matt Stein
Comments.ContentLicense: CC BY-SA 4.0
Comments.CreationDate: 2022-09-22T18:07:35.400Z
Comments.Text: @MattStein - thanks again for the reply and looking over the question, its really appreciated. I also, found out why the ?token= was not working. I had stupidly set a variable to the name of "token" which Craft seemed to dislike rather alot.
Comments.UserDisplayName: Wally
Comments.ContentLicense: CC BY-SA 4.0
Comments.CreationDate: 2022-09-22T23:44:22.233Z
Comments.Text: I dunno @Wally, “token” sounds reasonable to me even if it turned out to be a problem. It sounds like you’re closer to the goal, at least!
Comments.UserDisplayName: Matt Stein
Tags: <api><token>
Title: Using tokens to authenticate a one-time download link?
ThreadText: # Using tokens to authenticate a one-time download link?
  
  (Asked by: username_0 on 2022-09-20)
  
  I am trying to create a download link to an ASSET in craft that I can send via email - the client clicks the email link (which has a token query string attached to validate the link) and is presented with a Craft page to download the attachment.
  
  **But** I keep getting a 404 error, saying invalid route.
  
  * Im trying to create a token - which will only last for say 24H.
  
  `{% set token = craft.app.tokens.createToken() %}`
  
  I am aware that you need to pass in arguments to createToken() but I am not sure what they are as the docs are somewhat highlevel:
  
  <https://docs.craftcms.com/api/v4/craft-services-tokens.html#public-methods>
  
  * Store the token in Craft *(haven't worked this part out yet)*
  * Send a link, with the token as part of a query string
  
  `<a href="<PAGE_URL>?token={{ token }}">Download here</a>`
  
  * When the user visits the page, the twig template **1) gets the query string:**
  
    {% set token\_from\_queryString = craft.app.request.getQueryParams('token')|first %}
  * 2. Validates that the query string is the same as the one stored and so presents the download link to the user:
  
  ## LOGIC TEST
  
  ```
    <div>
      {% if token == token_from_queryString %}
        <p>Token matches - download file:</p>
      {% else %}
        <p>Seems that the link used is no longer acspected, please create a new one:</p>
      {% endif %}
    </div>  
  </div>
  
  ```
  
  Issues:
  
  1. I don't know if this is actually viable using this option.
  2. if I do not include the query string in the URL i.e. do not add ?token=(token value or a fake value) - then the page works as exspected. But if I add the query string I get given a 404 route error.
     How am I supposed to be formatting the tokens route, is this were the token lives? or a route to be directed to via the link. I haven't found any Craft docs on this as of yet.
  
  -- Update of work:
  
  I have got most of this code to work, but still am having issues with the route of the TOKEN and hiding the original URL of the ASSET.
  
  ISSUES:
  
  A) Does Craft automatically store tokens, if so where? I saw reference on discord to storing them in the DB, but no docs to back this up yet.
  
  B) I set the route to be the file URL in hopes it could be used in a `<a href="{{ tokenA }}" download>{{ download.title }}</a>`, but this says that the file cannot be found. But if I use "getToken" variable below, that uses `getTokenRoute()`, then it spits out the original URL of the download. So the token route is set to the URL (maybe I could be misunderstanding), but the token cannot be used to route to a download path.
  
  This is all with the goal of validating the user via a token and hiding the URL of the original download file.
  
  URL needing to be used:
  
  ```
  https://<your site address>/<page with this code on it>?tokenA=<the token number produced by tokenA (also printed to the page at TOKEN CODE:>
  
  ```
  
  Code below
  
  ```
  <main>
  <h2>TOKENs</h2>
  
  <div class="wrapper" style="background:lightblue">
    {# Get file #}
    {% set download = craft.assets().fileName('<NAME OF FILE TO BE USED AS ONE TIME DOWNLOAD>').one() %}
  
    {# Create token - ISSUE A) B) #}
    {% set tokenA = craft.app.tokens.createToken([download.url]) %}
  
    {# Gets the Token from the query string #}
    {% set queryString = craft.app.request.getQueryParams('tokenA')|first %}
  
    {# Takes the original token path - and gets it #}
    {# R) download path of the asset - download #}
    {% set getToken = craft.app.tokens.getTokenRoute(queryString)|first %}
  
    <p>TOKEN CODE: {{ tokenA }}</p>
    <a href="{{ tokenA }}">Download</a>
    <p>Result of get token: {{ getToken }}</p>
  </div>
  
  <div class="wrapper">
    <h2>Params</h2>
    <div class="wrapper" style="background:lightskyblue">
      <h1>{{ getToken }}</h1>
  
      {# If getToken has length i.e. it has been found #}
      {% if getToken|length %}
        <h1>Working</h1>
        {# <a href="{{ getToken }}">link</a> #}
      {% else %}
        <h1>Broken</h1>
      {% endif %}
  
        {# <p>getQueryString(): {{ param }}</p>
        <p>getQueryParams(): <br> 
          {% for block in param2 %}
            {{ block }}<br>
          {% endfor %}
        </p>
        <p><strong>getQueryParams('token')|first: {{ param4 }}</strong> <<< WINNER</p>
        <p>getQueryStringWithoutPath(): {{ param3 }}</p> #}
      
    </div>
  </div>
  
  <div class="wrapper">
    <h2>LOGIC TEST</h2>
  
    <div class="wrapper" style="background:lightyellow">
      {% if getToken|length %}
        {# If token is valid allow the user to download the below - #}
        {# ISSUE: need to hide the original url of the download so it cannot be shared - #}
        <a href="{{ download.url }}" download>{{ download.title }}</a>
  
        {# Delete the used token from the system (this doens't seem to be working) #}
        {{ craft.app.token.deleteTokenById(getToken) }}
      {% else %}
        <p>Seems that the link used is no longer acspected, please create a new one:</p>
      {% endif %}
    </div>  
  </div>
  
  ```
  
  **Tags:** api, token
  
  > **Comments:**
  > **username_1**: This _seems_ like it’d be a viable approach, where you’d want to store the token’s ID after creating it. That way, you could retrieve its value with `getTokenById()`. For issue #2, have you tried `&token=[value]` instead of `?token=[value]`? Don’t know offhand how the route should be formatted as an argument, but you’ll probably find examples in Craft’s source.  
  > **username_0**: Hi username_1, thanks for the help - turns out there is very little mention of createToken in the Craft source files - which has also been mentioned on the discord. Adding the & instead of the ? does work, but I think it now believe the path should include the token as part of the sites address and not a query string. I'll update the question with an example.  
  > **username_1**: To be fair, you’re working with something Craft uses more internally. The good news is there are examples in the source to follow, like this: https://github.com/craftcms/cms/blob/f45c185f4aa0d164f40fbdb34061d0150256ef1c/src/console/controllers/UsersController.php#L414-L419  You could mimic that pattern, including the use of that `UrlHelper::urlWithToken()` method to build the URL.  
  > **username_0**: @username_1 - thanks again for the reply and looking over the question, its really appreciated. I also, found out why the ?token= was not working. I had stupidly set a variable to the name of "token" which Craft seemed to dislike rather alot.  
  > **username_1**: I dunno @username_0, “token” sounds reasonable to me even if it turned out to be a problem. It sounds like you’re closer to the goal, at least!  
  
  ---
  
  ## 0 answers
  
  