Skip to content

New hotspot - Fortinet

The following describes how to configure a Fortinet router/firewall of type FortiGate 40f (FortiOS 7.4.6) and how to connect it to our Hotspot Solution SyCes.

Note: This is an early version of the SyCes2 software. Additional software features and support for additional hotspot router devices are currently being developed. This page documents the current status and possible uses of Fortinet firewalls (FortiGate) for the Hotspot Solution SyCes.

Prerequisites

To configure the router, you need administrative access to it. This tutorial shows the configuration via the GUI web interface of the firewal. This configuration could also be done via the CLI.

For configuration you also need information about the RADIUS IP and secret of the new system. This information can be read out from SyCes.

To set up the router for SyCes, the location that manages this router must exist in the new database. The tenant ID and the token created for the location are required for the configuration. If you want to read out these values, you need valid access data for SyCes.

Navigate to the corresponding tenant's location page and on the Locations details card, click the copy button next to the token in the Token field. The link in the URL field should look like this: /login/<tenant_id>/<location_id>/.

Step 1: Set up the network interface

First a network interface needs to be configured in the Network / Interfaces tab. The associated interface of the device should be equipped with a wireless access point (WAP) so that the customer devices can be connected via the interface after configuration.

For this purpose, the interface is set up so that the local captive portal is called up when the connection is established. Access of the interface is restricted to a User group that checks credentials with a set up RADIUS server.

The settings Role: LAN, the DHCP Server, the Security mode: Captive Portal, the User groups and the Excluded destinations/services: FQDN-backend.syces.de are particularly important. It is not required to toggle any checkbox under Administrative Access. RADIUS Accounting is set up in the RADIUS Server entry in Step 2. A User Group can only be assigned after it has been created (see Step 3). Therefore, updating the interface settings is required after step 3.

The final settings can be seen in the following images:

Image - Netzwerk Interface 1

Image - Netzwerk Interface 2

Step 2: Set up the RADIUS server

Under the User & Authentication / RADIUS Servers tab, the RADIUS server is set to the SyCes2 web server.

To do this, the RADIUS IP and the RADIUS secret must be specified. This information can be found at SyCes.

After entering the values into the respective fields (Primary Server > IP/Name and Secret) a connectivity test can be performed to validate the connection.

Image - Fortinet RADIUS Server

In order to use SyCes' time- or volume-limited account access, it may be necessary to manually set up a RADIUS Accounting configuration. To do this, you can open the RADIUS Server entry you just created in the Fortinet GUI. In the Edit RADIUS Server view, you can click on the Edit in CLI button on the right side.

The following variables must be set in the settings of the configured RADIUS Server:

config accounting-server
    edit 1
        set status enable
        set server "<RADIUS_IP>"
        set secret ENC <hashed_secret>
    next
end

You can copy the RADIUS IP and the Hashed Secret from the previous RADIUS Server settings. A working RADIUS Accounting configuration can be seen here:

Image - Fortinet RADIUS Server Accounting

Step 3: Set up the user group

Under the User & Authentication / User Groups tab, a new user group needs to be created to be assigned to the RADIUS server as Remote Groups as shown in the figure.

Image - Fortinet User Groups

Step 4: Customize the local captive portal (optional)

The local captive portal can be customized in the System / Replacement Messages tab. To do this, double-click on the Login Page entry to open a window with the HTML file.

Image - Fortinet Replacement Messages 1

The login page will appear on the left side of the new window. To customize this page, you can edit the code on the right. The login page on the left should be updated accordingly.

As an example customization, here is a page that used the SyCes2's anonymous login feature along with a login frame framework for pre-existing accounts:

Image - Fortinet Replacement Messages 2

Other pages of the captive portal and the logos can also be customized in this tab. In the following sections you can find examples of custom Login Pages and a Login Failed Page with a link back to the Login Page.

Login Page

Below is the website code for the custom login page shown above. You will also find the code for pages using only the Anonymous or Login function. Login pages in German can be found at Neuer Hotspot - Fortinet.

This code can be copied for functionality; However, the following fields still need to be replaced:

  • <tenant_id> (Replace this with your tenant's ID in SyCes)
  • <location_identification_token> (Replace this with the token of the configured Location in SyCes with router type Fortinet. This is required for the Anonymous function)
  • const tenant_domain = ""; (Set variable to enable automatic appending of the tenant domain to the username before login. Doing so enables the login with "account_name" instead of "account_name@tenant_domain". The domain can be copied from tenant page in SyCes.)
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=8; IE=EDGE">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style type="text/css">
      body {
        height: 100%;
        font-family: Helvetica, Arial, sans-serif;
        color: #6a6a6a;
        margin: 0;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      input[type=date], input[type=email], input[type=number], input[type=password], input[type=search], input[type=tel], input[type=text], input[type=time], input[type=url], select, textarea {
        color: #262626;
        vertical-align: baseline;
        margin: .2em;
        border-style: solid;
        border-width: 1px;
        border-color: #a9a9a9;
        background-color: #fff;
        box-sizing: border-box;
        padding: 2px .5em;
        appearance: none;
        border-radius: 0;
      }
      input:focus {
        border-color: #646464;
        box-shadow: 0 0 1px 0 #a2a2a2;
        outline: 0;
      }
      button {
        padding: .5em 1em;
        border: 1px solid;
        border-radius: 3px;
        min-width: 6em;
        font-weight: 400;
        font-size: .8em;
        cursor: pointer;
      }
      button.primary {
        color: #fff;
        background-color: #4698CB;
        border-color: #4698CB;
      }
      button.primary:hover {
        color: #fff;
        background-color: #005587;
        border-color: #005587;
      }
      button.primary:disabled{
        cursor:default;
        background-color: #E0E0E0;
        border-color: #E0E0E0;
      }
      .message-container {
        height: 500px;
        width: 500px;
        padding: 0;
        margin: 10px;
      }
      .logo {
        background: url(%%IMAGE:logo_v3_fguard_app%%) no-repeat left center;
        height: 267px;
        object-fit: contain;
      }
      .field {
        display: table-row;
      }
      .field > :first-child {
        display: table-cell;
        width: 20%;
      }
      .field.single > :first-child {
        display: inline;
      }
      .field > :not(:first-child) {
        width: auto;
        max-width: 100%;
        display: inline-flex;
        align-items: baseline;
        vertical-align: top;
        box-sizing: border-box;
        margin: .3em;
      }
      .field > :not(:first-child) > input {
        width: 230px;
      }
      .form-footer {
        display: inline-flex;
        justify-content: flex-start;
      }
      .form-footer > * {
        margin: 1em;
      }
      .text-scrollable {
        overflow: auto;
        height: 150px;
        border: 1px solid rgb(200, 200, 200);
        padding: 5px;
        font-size: 1em;
      }
      .text-centered {
        text-align: center;
      }
      .text-container {
        margin: 1em 1.5em;
      }
      .flex-container {
        display: flex;
      }
      .flex-container.column {
        flex-direction: column;
      }
    </style>
    <title>
      SyCes ® - Hotspot
    </title>
  </head>
  <body>
    <div class="message-container">
      <div class="logo"></div>
      <h1>
        Register anonymously
      </h1>
      <form id="anon_login" >
        <div>
          <input type="checkbox" name="tos" id="tos_check_reg" onclick="handleTOSClick(this)">
          I agree to the <a href="https://backend.syces.de/login/<tenant_id>/tos/">Terms and conditions</a>
        </div>
        <div class="form-footer">
          <button class="primary" type="submit" id="submit_btn_reg" disabled>
            Submit
          </button>
        </div>
      </form>
      <form
        id="default_login"
        action="%%AUTH_POST_URL%%"
        method="post"
      >
        <input type="hidden" name="%%REDIRID%%" value="%%PROTURI%%">
        <input type="hidden" name="%%MAGICID%%" value="%%MAGICVAL%%">
        <input type="hidden" name="%%METHODID%%" value="%%METHODVAL%%">
        <h1>
          Login
        </h1>
        <div class="field">
          <label for="ft_un">
            Username
          </label>
          <div>
            <input name="%%USERNAMEID%%" id="ft_un" type="text" autocorrect="off" autocapitalize="off">
          </div>
        </div>
        <div class="field">
          <label for="ft_pd">
            Password
          </label>
          <div>
            <input name="%%PASSWORDID%%" id="ft_pd" type="password" autocomplete="off">
          </div>
        </div>
        <div>
          <input type="checkbox" name="tos" id="tos_check_login" onclick="handleTOSClick(this)">
          I agree to the <a href="https://backend.syces.de/login/<tenant_id>/tos/">Terms and conditions</a>
        </div>
        <div class="form-footer">
          <button class="primary" type="submit" id="submit_btn_login" disabled>
            Submit
          </button>
        </div>
      </form>
    </div>
  </body>
  <script>
    const url = "https://backend.syces.de/login/token_login/<location_identification_token>/"

    const anon_form = document.getElementById("anon_login");
    const default_form = document.getElementById("default_login");

    const tos_checkbox_reg = document.getElementById("tos_check_reg");
    const tos_checkbox_login = document.getElementById("tos_check_login");
    const submit_button_reg = document.getElementById("submit_btn_reg");
    const submit_button_login = document.getElementById("submit_btn_login");

    function handleTOSClick(cb) {
      if (cb.checked === true) {
        tos_checkbox_reg.checked = true;
        tos_checkbox_login.checked = true;
        submit_button_reg.disabled = false;
        submit_button_login.disabled = false;
      } else {
        tos_checkbox_reg.checked = false;
        tos_checkbox_login.checked = false;
        submit_button_reg.disabled = true;
        submit_button_login.disabled = true;
      }
    }

    function sendRequest() {
      fetch(url)
      .then(response => response.json())
      .then(data => {
        let field_username = document.getElementById("ft_un");
        let field_password = document.getElementById("ft_pd");
        field_username.value = data.login_name;
        field_password.value = data.password;
        default_form.submit();
      })
      .catch(error => console.error(error));
    }

    function appendDomainIfMissing() {
      const tenant_domain = "";
      let form_input_user = document.getElementById("ft_un");
      let username = form_input_user.value;

      if (
        tenant_domain!== "" &&
        !username.includes("@") &&
        !username.endsWith("@" + tenant_domain)
      ) {
        form_input_user.value = username + "@" + tenant_domain;
      }
    }

    anon_form.addEventListener('submit', function(e) {
      e.preventDefault();
      sendRequest();
    })

    default_form.addEventListener('submit', function(e) {
      appendDomainIfMissing();
    })
  </script>
</html>
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=8; IE=EDGE">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style type="text/css">
      body {
        height: 100%;
        font-family: Helvetica, Arial, sans-serif;
        color: #6a6a6a;
        margin: 0;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      input[type=date], input[type=email], input[type=number], input[type=password], input[type=search], input[type=tel], input[type=text], input[type=time], input[type=url], select, textarea {
        color: #262626;
        vertical-align: baseline;
        margin: .2em;
        border-style: solid;
        border-width: 1px;
        border-color: #a9a9a9;
        background-color: #fff;
        box-sizing: border-box;
        padding: 2px .5em;
        appearance: none;
        border-radius: 0;
      }
      input:focus {
        border-color: #646464;
        box-shadow: 0 0 1px 0 #a2a2a2;
        outline: 0;
      }
      button {
        padding: .5em 1em;
        border: 1px solid;
        border-radius: 3px;
        min-width: 6em;
        font-weight: 400;
        font-size: .8em;
        cursor: pointer;
      }
      button.primary {
        color: #fff;
        background-color: #4698CB;
        border-color: #4698CB;
      }
      button.primary:hover {
        color: #fff;
        background-color: #005587;
        border-color: #005587;
      }
      button.primary:disabled{
        cursor:default;
        background-color: #E0E0E0;
        border-color: #E0E0E0;
      }
      .message-container {
        height: 500px;
        width: 500px;
        padding: 0;
        margin: 10px;
      }
      .logo {
        background: url(%%IMAGE:logo_v3_fguard_app%%) no-repeat left center;
        height: 267px;
        object-fit: contain;
      }
      .field {
        display: table-row;
      }
      .field > :first-child {
        display: table-cell;
        width: 20%;
      }
      .field.single > :first-child {
        display: inline;
      }
      .field > :not(:first-child) {
        width: auto;
        max-width: 100%;
        display: inline-flex;
        align-items: baseline;
        vertical-align: top;
        box-sizing: border-box;
        margin: .3em;
      }
      .field > :not(:first-child) > input {
        width: 230px;
      }
      .form-footer {
        display: inline-flex;
        justify-content: flex-start;
      }
      .form-footer > * {
        margin: 1em;
      }
      .text-scrollable {
        overflow: auto;
        height: 150px;
        border: 1px solid rgb(200, 200, 200);
        padding: 5px;
        font-size: 1em;
      }
      .text-centered {
        text-align: center;
      }
      .text-container {
        margin: 1em 1.5em;
      }
      .flex-container {
        display: flex;
      }
      .flex-container.column {
        flex-direction: column;
      }
    </style>
    <title>
      SyCes ® - Hotspot
    </title>
  </head>
  <body>
    <div class="message-container">
      <div class="logo"></div>
      <form
        id="default_login"
        action="%%AUTH_POST_URL%%"
        method="post"
      >
        <input type="hidden" name="%%REDIRID%%" value="%%PROTURI%%">
        <input type="hidden" name="%%MAGICID%%" value="%%MAGICVAL%%">
        <input type="hidden" name="%%METHODID%%" value="%%METHODVAL%%">
        <h1>
          Login
        </h1>
        <div class="field">
          <label for="ft_un">
            Username
          </label>
          <div>
            <input name="%%USERNAMEID%%" id="ft_un" type="text" autocorrect="off" autocapitalize="off">
          </div>
        </div>
        <div class="field">
          <label for="ft_pd">
            Password
          </label>
          <div>
            <input name="%%PASSWORDID%%" id="ft_pd" type="password" autocomplete="off">
          </div>
        </div>
        <div>
          <input type="checkbox" name="tos" id="tos_check_login" onclick="handleTOSClick(this)">
          I agree to the <a href="https://backend.syces.de/login/<tenant_id>/tos/">Terms and conditions</a>
        </div>
        <div class="form-footer">
          <button class="primary" type="submit" id="submit_btn_login" disabled>
            Submit
          </button>
        </div>
      </form>
    </div>
  </body>
  <script>
    const default_form = document.getElementById("default_login");

    const tos_checkbox_login = document.getElementById("tos_check_login");
    const submit_button_login = document.getElementById("submit_btn_login");

    function handleTOSClick(cb) {
      if (cb.checked === true) {
        tos_checkbox_login.checked = true;
        submit_button_login.disabled = false;
      } else {
        tos_checkbox_login.checked = false;
        submit_button_login.disabled = true;
      }
    }

    function appendDomainIfMissing() {
      const tenant_domain = "";
      let form_input_user = document.getElementById("ft_un");
      let username = form_input_user.value;

      if (
        tenant_domain!== "" &&
        !username.includes("@") &&
        !username.endsWith("@" + tenant_domain)
      ) {
        form_input_user.value = username + "@" + tenant_domain;
      }
    }

    default_form.addEventListener('submit', function(e) {
      appendDomainIfMissing();
    })
  </script>
</html>
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=8; IE=EDGE">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style type="text/css">
      body {
        height: 100%;
        font-family: Helvetica, Arial, sans-serif;
        color: #6a6a6a;
        margin: 0;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      input[type=date], input[type=email], input[type=number], input[type=password], input[type=search], input[type=tel], input[type=text], input[type=time], input[type=url], select, textarea {
        color: #262626;
        vertical-align: baseline;
        margin: .2em;
        border-style: solid;
        border-width: 1px;
        border-color: #a9a9a9;
        background-color: #fff;
        box-sizing: border-box;
        padding: 2px .5em;
        appearance: none;
        border-radius: 0;
      }
      input:focus {
        border-color: #646464;
        box-shadow: 0 0 1px 0 #a2a2a2;
        outline: 0;
      }
      button {
        padding: .5em 1em;
        border: 1px solid;
        border-radius: 3px;
        min-width: 6em;
        font-weight: 400;
        font-size: .8em;
        cursor: pointer;
      }
      button.primary {
        color: #fff;
        background-color: #4698CB;
        border-color: #4698CB;
      }
      button.primary:hover {
        color: #fff;
        background-color: #005587;
        border-color: #005587;
      }
      button.primary:disabled{
        cursor:default;
        background-color: #E0E0E0;
        border-color: #E0E0E0;
      }
      .message-container {
        height: 500px;
        width: 500px;
        padding: 0;
        margin: 10px;
      }
      .logo {
        background: url(%%IMAGE:logo_v3_fguard_app%%) no-repeat left center;
        height: 267px;
        object-fit: contain;
      }
      .field {
        display: table-row;
      }
      .field > :first-child {
        display: table-cell;
        width: 20%;
      }
      .field.single > :first-child {
        display: inline;
      }
      .field > :not(:first-child) {
        width: auto;
        max-width: 100%;
        display: inline-flex;
        align-items: baseline;
        vertical-align: top;
        box-sizing: border-box;
        margin: .3em;
      }
      .field > :not(:first-child) > input {
        width: 230px;
      }
      .form-footer {
        display: inline-flex;
        justify-content: flex-start;
      }
      .form-footer > * {
        margin: 1em;
      }
      .text-scrollable {
        overflow: auto;
        height: 150px;
        border: 1px solid rgb(200, 200, 200);
        padding: 5px;
        font-size: 1em;
      }
      .text-centered {
        text-align: center;
      }
      .text-container {
        margin: 1em 1.5em;
      }
      .flex-container {
        display: flex;
      }
      .flex-container.column {
        flex-direction: column;
      }
    </style>
    <title>
      SyCes ® - Hotspot
    </title>
  </head>
  <body>
    <div class="message-container">
      <div class="logo"></div>
      <h1>Register anonymously</h1>
      <form id="anon_login" >
        <div>
          <input type="checkbox" name="tos" id="tos_check_reg" onclick="handleTOSClick(this)">
          I agree to the <a href="https://backend.syces.de/login/<tenant_id>/tos/">Terms and conditions</a>
        </div>
        <div class="form-footer">
          <button class="primary" type="submit" id="submit_btn_reg" disabled>
            Submit
          </button>
        </div>
      </form>
      <form
        id="default_login"
        action="%%AUTH_POST_URL%%"
        method="post"
        style="display:none"
      >
        <input type="hidden" name="%%REDIRID%%" value="%%PROTURI%%">
        <input type="hidden" name="%%MAGICID%%" value="%%MAGICVAL%%">
        <input type="hidden" name="%%METHODID%%" value="%%METHODVAL%%">
        <h1>
          Login
        </h1>
        <div class="field">
          <label for="ft_un">
            Username
          </label>
          <div>
            <input name="%%USERNAMEID%%" id="ft_un" type="text" autocorrect="off" autocapitalize="off">
          </div>
        </div>
        <div class="field">
          <label for="ft_pd">
            Password
          </label>
          <div>
            <input name="%%PASSWORDID%%" id="ft_pd" type="password" autocomplete="off">
          </div>
        </div>
        <div>
          <input type="checkbox" name="tos" id="tos_check_login" onclick="handleTOSClick(this)">
        </div>
        <div class="form-footer">
          <button class="primary" type="submit" id="submit_btn_login" disabled>
            Submit
          </button>
        </div>
      </form>
    </div>
  </body>
  <script>
    const url = "https://backend.syces.de/login/token_login/<location_identification_token>/"

    const anon_form = document.getElementById("anon_login");
    const default_form = document.getElementById("default_login");

    const tos_checkbox_reg = document.getElementById("tos_check_reg");
    const tos_checkbox_login = document.getElementById("tos_check_login");
    const submit_button_reg = document.getElementById("submit_btn_reg");
    const submit_button_login = document.getElementById("submit_btn_login");

    function handleTOSClick(cb) {
      if (cb.checked === true) {
        tos_checkbox_reg.checked = true;
        tos_checkbox_login.checked = true;
        submit_button_reg.disabled = false;
        submit_button_login.disabled = false;
      } else {
        tos_checkbox_reg.checked = false;
        tos_checkbox_login.checked = false;
        submit_button_reg.disabled = true;
        submit_button_login.disabled = true;
      }
    }

    function sendRequest() {
      fetch(url)
      .then(response => response.json())
      .then(data => {
        let field_username = document.getElementById("ft_un");
        let field_password = document.getElementById("ft_pd");
        field_username.value = data.login_name;
        field_password.value = data.password;
        default_form.submit();
        }
      )
      .catch(error => console.error(error));
    }

    anon_form.addEventListener('submit', function(e) {
      e.preventDefault();
      sendRequest();
    })
  </script>
</html>

"Login Failed" Page

The "Login Failed" Page is called when an authentication attempt of the Login Page fails. For a positive user experience, this page should contain either a link back to the Login Page or another way to log in.

Therefore, we recommend copying the same code for both pages or a page with a redirect link as shown below.

If you want to enable automated redirection with the following code, you can set the variable const seconds_to_redirect = 0; to a desired value. We recommend 5 - 10 seconds for the delay.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=8; IE=EDGE">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style type="text/css">
      body {
        height: 100%;
        font-family: Helvetica, Arial, sans-serif;
        color: #6a6a6a;
        margin: 0;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      .message-container {
        height: 500px;
        width: 500px;
        padding: 0;
        margin: 10px;
      }
      .logo {
        background: url(%%IMAGE:logo_v3_fguard_app%%) no-repeat left center;
        height: 267px;
        object-fit: contain;
      }
      .text-scrollable {
        overflow: auto;
        height: 150px;
        border: 1px solid rgb(200, 200, 200);
        padding: 5px;
        font-size: 1em;
      }
      .text-centered {
        text-align: center;
      }
      .text-container {
        margin: 1em 1.5em;
      }
      .flex-container {
        display: flex;
      }
      .flex-container.column {
        flex-direction: column;
      }
    </style>
    <title>
      SyCes ® - Hotspot Login Failed
    </title>
  </head>
  <body>
    <div class="message-container">
      <div class="logo">
      </div>
      <h1>
        Authentication Failed
      </h1>
      <p>
        User authentication failed. Please return to <a href="%%PROTURI%%">login page</a> and try again.
      </p>
    </div>
  </body>
  <script>
    const seconds_to_redirect = 0;
    if (seconds_to_redirect > 0) {
      window.setTimeout(function(){
          window.location.href = "%%PROTURI%%";
        }, seconds_to_redirect*1000
      );
    }
  </script>
</html>

Step 5: Login attempt

After setting up a Fortinet firewall with these settings, connecting a device to the configured interface's Wi-Fi should open the customized captive portal.

When you click the Submit button of the Anonymous feature, the SyCes web server is called to create an account whose credentials are sent back to the website on your device so that it can attempt to log the device in using the returned credentials.

Troubleshooting

If you experience any problems or unexpected behavior when setting up your router, please contact us via [email protected].