Keycloak

  • Keycloak IdP for SSO

    To explore : http://www.keycloak.org/docs/latest/securing_apps/index.html#_mod_auth_openidc

    official website

    Server IdP ( identity provider) for the SSO (single sign-on) Application written in Java, runs on a WildFly server.

    Installation and configuration

    Pre requises (see documentation) :

    Fetch the software :

    sudo su
    add-apt-repository ppa:webupd8team/java
    apt update
    apt install default-jre oracle-java8-installer

    Fetch the software :

    cd /var/www
    wget https://downloads.jboss.org/keycloak/xxx.Final/keycloak-xxx.Final.zip
    unzip keycloak-xxx.Final.zip -d .
    cd keycloak-xxx.Final
    wget https://downloads.jboss.org/keycloak/xxx.Final/adapters/keycloak-oidc/keycloak-wildfly-adapter-dist-xxx.Final.zip
    unzip keycloak-wildfly-adapter-dist-xxx.Final.zip -d .
    rm *.zip

    Add the user admin :

    ./bin/add-user-keycloak.sh -u username

    if it returns the message No content to map due to end-of-input, then create a temporary admin user tempuser with the password dkPK62wdofPEK56-_&SO :

    nano ./standalone/configuration/keycloak-add-user.json

    Put this :

    [ {
      "realm" : "master",
      "users" : [{
        "username" : "tempuser",
        "enabled" : true,
        "emailVerified" : true,
        "credentials" : [ {
          "type" : "password",
          "hashedSaltedValue" : "IhS5rpj4PV20PURXYtQktPAYDbI5ATQdafxquCWs1mKwJtuvxhW2DQ0QGNuaQV42MXLSkrLeAyjf4UzlIxm04g==",
          "salt" : "wue854XfHBduqKFcgm9tNQ==",
          "hashIterations" : 100000,
          "algorithm" : "pbkdf2-sha256"
        } ],
        "realmRoles" : [ "admin" ]
      } ]
    } ]

    Start the server :

    ./bin/standalone.sh

    When you experience an error at startup, check if it is not a port which is already taken, like port 8080 (we use 8081) or 9990 (we use 9991) In tat case change the config file (at the end) :

    nano standalone/configuration/standalone.xml

    Access server from outside

    Open the ports (adapt the numbers) :

    ufw allow 8081
    ufw allow 9991

    Launch the server on the IP of the network card of the machine :

    ifconfig

    note the IP of the network card (network adapter)

    nano standalone/configuration/standalone.xml

    search this phrase : jboss.bind.address.management: and change 127.0.0.1 with the IP address of the network card. Idem for jboss.bind.address

    Start the server and go to the address x.x.x.x:8081

    Configure SSL to access to the server with HTTPS

    With Apache reverse proxy.

    nano standalone/configuration/standalone.xml

    In <http-listener, add proxy-address-forwarding="true" to have :

    <http-listener name="default" socket-binding="http" redirect-socket="proxy-https" proxy-address-forwarding="true" enable-http2="true"/>
    sudo a2enmod headers
    cd /etc/apache2/sites-available

    Create the file keycloak.domain.ext.conf

    <VirtualHost *:80>
            ServerName keycloak.domain.ext
            ServerAdmin email@domain.ext
    
            ProxyPass / http://000.000.000.000:8081/
            ProxyPassReverse / http://000.000.000.000:8081/
            ProxyRequests Off
            ProxyPreserveHost Off
    
            ErrorLog ${APACHE_LOG_DIR}/error.log
            CustomLog ${APACHE_LOG_DIR}/access.log combined
    </VirtualHost>
    a2ensite keycloak.domain.ext.conf 
    service apache2 reload
    certbot --apache -d keycloak.domain.ext

    Edit keycloak.domain.ext-le-ssl and add inside the <VirtualHost *:443> tag :

            ProxyPreserveHost On
            SSLProxyEngine On
            SSLProxyCheckPeerCN on
            SSLProxyCheckPeerExpire on
            RequestHeader set X-Forwarded-Proto "https"
            RequestHeader set X-Forwarded-Port "443"
    service apache2 reload

    Configure the MySQL database

    Similar tutorial

    sudo su
    cd /var/www/keycloak
    mkdir ./modules/system/layers/keycloak/com/mysql
    mkdir ./modules/system/layers/keycloak/com/mysql/main

    Download the Java module of MySQL (select "Platform independant"). Latest working version is : 5.1.45. So you can directely download with wget https://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-java-5.1.45.zip.

    Unzip and copy the file mysql-connector-java-x.x.x-bin.jar to ./modules/system/layers/keycloak/com/mysql/main, rename it to mysql-connector-java-bin.jar and make it executable : chmod +x modules/system/layers/keycloak/com/mysql/main/mysql-connector-java-bin.jar

    nano ./modules/system/layers/keycloak/com/mysql/main/module.xml and put :

    <?xml version="1.0" ?>
    <module xmlns="urn:jboss:module:1.3" name="com.mysql">
    
        <resources>
            <resource-root path="mysql-connector-java-bin.jar"/>
        </resources>
    
        <dependencies>
            <module name="javax.api"/>
            <module name="javax.transaction.api"/>
        </dependencies>
    </module>

    Create the database idp_keycloak (don't use - char) with utf8mb4_general_ci, create user idp_keycloak and give him the rights of this this base.

    Caution ! Avoid special characters in the MySQL password like the & char.

    Edit the file standalone/configuration/standalone.xml, replace what is between <subsystem xmlns="urn:jboss:domain:datasources:x.x"> and </subsystem> with (replace MYSQL-USER-PASSWORD) :

                <datasources>
                    <datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" enabled="true" use-java-context="true" statistics-enabled="${wildfly.datasources.statistics-enabled:${wildfly.statistics-enabled:false}}">
                       <connection-url>jdbc:mysql://localhost/idp_keycloak?useSSL=false&amp;characterEncoding=UTF-8</connection-url>
                       <driver>mysql</driver>
                       <pool>
                           <max-pool-size>20</max-pool-size>
                       </pool>
                       <security>
                           <user-name>idp_keycloak</user-name>
                           <password>MYSQL-USER-PASSWORD</password>
                       </security>
                    </datasource>
                    <datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true">
                        <connection-url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</connection-url>
                        <driver>h2</driver>
                        <security>
                            <user-name>sa</user-name>
                            <password>sa</password>
                        </security>
                    </datasource>
                    <drivers>
                      <driver name="mysql" module="com.mysql">
                          <driver-class>com.mysql.jdbc.Driver</driver-class>
                      </driver>      
                      <driver name="h2" module="com.h2database.h2">
                          <xa-datasource-class>org.h2.jdbcx.JdbcDataSource</xa-datasource-class>
                      </driver>
                    </drivers>
                </datasources>

    Then, before restarting the server, add again a new admin user (because he was registered in the old database) :

        bin/add-user-keycloak.sh -u username

    Launch the Widfly server when the machine starts

    sudo nano /etc/rc.local

    and add : /var/www/keycloak/keycloak-server/bin/standalone.sh &

    Currently under development

    https://keycloak.gitbooks.io/documentation/getting_started/topics/secure-jboss-app/install-client-adapter.html

        cd /var/www/keycloak
        wget https://downloads.jboss.org/keycloak/3.2.1.Final/adapters/keycloak-oidc/keycloak-wildfly-adapter-dist-3.2.1.Final.zip
        unzip keycloak-wildfly-adapter-dist-3.2.1.Final.zip -d .
        rm keycloak-wildfly-adapter-dist-3.2.1.Final.zip
        cd bin
        ./jboss-cli.sh --file=adapter-install-offline.cli

    Add an IdP provider

    Example with Facebook

    Realm configuration

    Configure :

    • Realm settings :
      • Login : all "on" except "email as username" or "Edit username" ; require SSL -> all requests
      • Email
      • Theme : enable internationalization and select "fr" only
      • Security Defenses (if iframe) : replace Content-Security-Policy by frame-src 'self' domain.ext sub.domain.ext; frame-ancestors 'self' domain.ext sub.domain.ext; object-src 'self' domain.ext sub.domain.ext;
      • Tokens : SSO session max : 365 days / Offline Session Idle : 400 days
    • Authentification :
      • Passowrd Policy : Digits 1 / Minimum Length : 8 / Not Username / Uppercase Characters : 1

    Create a custom theme

    Official documentation Tutorial

    To create theme, remove caching

    nano standalone/configuration/standalone.xml :

                <theme>
                    <staticMaxAge>-1</staticMaxAge>
                    <cacheThemes>false</cacheThemes>
                    <cacheTemplates>false</cacheTemplates>

    select the custom theme

    For the welcome page : nano standalone/configuration/standalone.xml :

                <theme>
                    <welcomeTheme>custom-theme</welcomeTheme>

    For the other pages : go in the administration -> Domain configuration -> theme

    When your theme is ready, put back caching

    nano standalone/configuration/standalone.xml :

                <theme>
                    <staticMaxAge>2592000</staticMaxAge>
                    <cacheThemes>true</cacheThemes>
                    <cacheTemplates>true</cacheTemplates>

    Mass import users

    Create a json file containing, for example (password is dkPK62wdofPEK56-_&SO) :

    {
      "realm": "master",
      "users" : [
        {
          "username" : "test1",
          "email" : "test1@mail.ext",
          "firstName" : "Toto1",
          "lastName" : "Titi1",
          "enabled" : true,
          "emailVerified" : true,
          "credentials" : [ {
            "type" : "password",
            "hashedSaltedValue" : "IhS5rpj4PV20PURXYtQktPAYDbI5ATQdafxquCWs1mKwJtuvxhW2DQ0QGNuaQV42MXLSkrLeAyjf4UzlIxm04g==",
            "salt" : "wue854XfHBduqKFcgm9tNQ==",
            "hashIterations" : 100000,
            "algorithm" : "pbkdf2-sha256"
          } ],
          "realmRoles" : [ "offline_access", "uma_authorization" ]
        },
        {
          "username" : "test2",
          "email" : "test2@mail.ext",
          "firstName" : "Toto2",
          "lastName" : "Titi2",
          "enabled" : true,
          "emailVerified" : true,
          "credentials" : [ {
            "type" : "password",
            "hashedSaltedValue" : "IhS5rpj4PV20PURXYtQktPAYDbI5ATQdafxquCWs1mKwJtuvxhW2DQ0QGNuaQV42MXLSkrLeAyjf4UzlIxm04g==",
            "salt" : "wue854XfHBduqKFcgm9tNQ==",
            "hashIterations" : 100000,
            "algorithm" : "pbkdf2-sha256"
          } ],
          "realmRoles" : [ "offline_access", "uma_authorization" ]
        }
      ]
    }

    And import it in the admin user interface (Manage -> Import)

    Create a new client

    Infos : https://domain.ext:8443/auth/realms/master/.well-known/openid-configuration

    More info :

    The Reaml X.509 Certificate can be found in Realm settings -> Keys -> Certificate

    Logout URL : https://domain.ext:8443/auth/realms/master/protocol/openid-connect/logout?redirect_uri=encodedURL

    OpenId Connect

    • Client Protocol : openid-connect
    • Access Type : confidential
    • Standard Flow Enabled : ON
    • Implicit Flow Enabled : OFF
    • Direct Access Grants Enabled : ON
    • Service Accounts Enabled : OFF
    • Authorization Enabled : OFF
    • Root URL : htts://domain.ext
    • Valid Redirect URIs : htts://domain.ext/*
    • Fine Grain OpenID Connect Configuration :
      • User Info Signed Response Algorithm : unsigned
      • Request Object Signature Algorithm : any

    Wordpress OpenID Connect Login

    1. Create an OpenId Connect client
    2. Install OpenId Connect Generic plugin
    3. Configure :
    1. Go to "Settings" -> "Permalinks" and save again the configuration (even if no changes)
    2. Add the button : [openid_connect_generic_login_button]
    3. In openid-connect-generic-client-wrapper.php : replace the pattern of preg_replace by '/[^a-zA-Z\-\_0-9]/'
    4. In openid-connect-generic-login-form.php : replace Login with OpenID Connect by Login / Sign up
    5. in openid-connect-generic-client.php :
    • remove all the if after // check the client request state
    • replace : $id_token_claim = json_decode( base64_decode( $tmp[1] ), TRUE ); by :
    		$id_token_claim = json_decode(
    			base64_decode(
    				str_replace( // because token is encoded in base64 URL (and not just base64)
    					array('-', '_'),
    					array('+', '/'),
    					$tmp[1]
    				)
    			)
    			, TRUE
    		);

    If doesn't work because of "cURL connection refused" see this workaround

    If you want users to connect to Wordpress throw an iframe, add this in the theme function.php :

    // Allow to connect from an iframe
    remove_action( 'login_init', 'send_frame_options_header' );
    remove_action( 'admin_init', 'send_frame_options_header' );

    Wordpress SAML OneLogin plugin

    Change "master" with your Realm name if different, and 8443 with your custom port if different

    In OneLogin settings :

    • IdP Identity id : https://URL:8443/auth/realms/master
    • Single Sign On Service Url : https://URL:8443/auth/realms/master/protocol/saml
    • X.509 Certificate : Keycloak -> Realm settings -> Keys -> Certificate
    • ATTRIBUTE MAPPING :
      • Username : username
      • E-mail : email
      • First Name : first_name
      • Last Name : last_name
      • Role : Role
    • ROLE PRECEDENCE : Administrator : 1, Editor : 2, etc.
    • Service Provider Entity Id : Keycloak -> Client -> Client ID
    • Sign AuthnRequest : Checked
    • Sign LogoutRequest : Checked
    • Sign LogoutResponse : Checked
    • NameIDFormat : ... SAML:2.0 ... persistent
    • Service Provider X.509 Certificate & Service Provider Private Key : put a temporary cerficificate
    • Signature Algorithm & Digest Algorithm: RSA_SHA256

    Click on "Go to the metadata of this SP" (top right) : download in a XML file

    In KeyCloak : add a new client

    • Select file : the XML file and save

    If you need to configure manually :

    Tab Mappers

    • Roles list -> edit :
      • Role attribute name : Role
      • SAML Attribute NameFormat : unspecified
      • Single Role Attribute : ON
    • Create :
      • Name : username
      • Mapper Type : User Property
      • Property : username
      • Friendly Name : username
      • SAML Attribute Name : username
      • SAML Attribute NameFormat : unspecified
    • Create again for : email, first_name, last_name

    Keycloak -> Client -> Client Id -> SAML Keys -> Generate new keys

    Copy values in Onelogin plugin -> Service Provider X.509 Certificate & Service Provider Private Key

    OpenID-Connect-PHP

    OpenID-Connect-PHP GitHub repository

    It's a simple library that allows an application to authenticate a user through the basic OpenID Connect flow.

    Install :

    composer require jumbojett/openid-connect-php

    Create a test script test.php

    <?php
    ini_set('display_errors', 1);
    ini_set('display_startup_errors', 1);
    error_reporting(E_ALL);
    
    require "./vendor/autoload.php";
    
    $oidc = new OpenIDConnectClient('https://idp.make.social:8443/auth/realms/master',
                                    'OpenID-Connect-PHP',
                                    'b16fbf2b-7aa3-4f02-a488-d3e04f26cb3e');
    
    $oidc->authenticate();
    $userInfo = $oidc->requestUserInfo();
    $name = $userInfo->preferred_username; // see KeyCloak client Mappers tab (field "Token Claim Name")
    ?>
    
    <html>
    <head>
        <title>Example OpenID Connect Client Use</title>
        <style>
            body {
                font-family: 'Lucida Grande', Verdana, Arial, sans-serif;
            }
        </style>
    </head>
    <body>
    
        <div>
            <p>Hello <?php echo $name; ?></p>
            <p>UserInfo array : <?php print_r($userInfo); ?></p>
        </div>
    
    </body>
    </html>

    Yii2 OpenIdConnect

    Install

    Source

    composer require --prefer-dist yiisoft/yii2-authclient "~2.1.0"

    Edit config/web.php and add in 'compenents' :

            'authClientCollection' => [
                'class' => 'yii\authclient\Collection',
                'clients' => [
                    'YourName' => [
                        'class' => 'yii\authclient\clients\Keycloak',
                        'issuerUrl' => 'https://domain.ext:8443/auth/realms/master',
                        'clientId' => 'KeycloakClientId',
                        'clientSecret' => '*************',
                    ],
                ],
            ],

    Start

    Source

    Open the file controllers/SiteController.php, add use app\components\AuthHandler; at the beginning of the file, and theses 2 functions inside class SiteController extends Controller :

        public function actions()
        {
            return [
                'auth' => [
                    'class' => 'yii\authclient\AuthAction',
                    'successCallback' => [$this, 'onAuthSuccess'],
                ],
            ];
        }
    
        public function onAuthSuccess($client)
        {
            (new AuthHandler($client))->handle();
        }

    The class class AuthHandler must be in components/AuthHandler.php (create components folder if not exist) and create the file AuthHandler.php containing the implementation describe here

    Add the widget in a view file (eg. views/site/login.php) :

    <?= yii\authclient\widgets\AuthChoice::widget([
         'baseAuthUrl' => ['site/auth'],
         'popupMode' => false,
    ]) ?>

    Install OpenId Connect

    Source

    sudo apt update;
    sudo apt install libgmp-dev php-gmp;
    sudo service apache2 reload;
    composer require --prefer-dist "spomky-labs/jose:~5.0.6"

    The class OpenIdConnect extends OAuth2 See OpenId Connect specifications

    Create the Auth.php file if not exist

    nano models/Auth.php :

    <?php
    namespace app\models;
    use Yii;
    class Auth extends \yii\db\ActiveRecord
    {
        public static function tableName()
        {
            return 'user_auth';
        }
    
        public function rules()
        {
            return [
                [['user_id', 'source', 'source_id'], 'required'],
                [['user_id'], 'integer'],
                [['source', 'source_id'], 'string', 'max' => 255]
            ];
        }
    
        public function attributeLabels()
        {
            return [
                'id' => 'ID',
                'user_id' => 'User ID',
                'source' => 'Source',
                'source_id' => 'Source ID',
            ];
        }
    
        public function getUser()
        {
            return $this->hasOne(User::className(), ['id' => 'user_id']);
        }
    }
    

    Create a Keycloak client

    Create the file vendor/yiisoft/yii2-authclient/clients/Keycloak.php containing :

    <?php
    
    namespace yii\authclient\clients;
    
    use yii\authclient\OpenIdConnect;
    
    class Keycloak extends OpenIdConnect
    {
        public function applyAccessTokenToRequest($request, $accessToken)
        {
            $data = $request->getData();
            $data['Authorization'] = 'Bearer ' . $accessToken->getToken();
            $request->setHeaders($data);
        }
    
    
        protected function defaultName()
        {
            return 'keycloak';
        }
    
        protected function defaultTitle()
        {
            return 'Centralized authentification';
        }
    }

    Debug

    vendor/yiisoft/yii2-authclient/OpenIdConnect.php

    At the end of function requestTokens($code) :

    $return = parent::fetchAccessToken($authCode, $params);
    file_put_contents("/var/www/test.make.social/public_html/test.txt", serialize($return));
    return $return;

    cat /var/www/test.make.social/public_html/test.txt

    Copy and paste the token (just after access_token";s:1746:") in https://jwt.io/

    Node.Js with Oauth2

    https://adodson.com/hello.js

    Symfony OpenIdConnect

    1. Install Symfony 2.8 demo
    2. Install OpenId Connect Relying Party Bundle but replace "gree/jose": "0.1.7" with "gree/jose" : "~2.0"

    Example of config for :

        base_url: "https://sso-synfony.marc.fun"
        client_id: "sso-symfony-marc-fun"         #OpenID Connect client id given by the OpenId Connect Provider
        client_secret: "xxxxxxxx-xxxx-xxxx-xxxxx-xxxxxxxx" #OpenID Connect client secret given by the OpenId Connect Provider
        issuer: "https://login.lescommuns.org:8443/auth/realms/master/protocol/openid-connect" #URL of the OpenID Connect Provider
        endpoints_url:                  #Part of the URL of the OpenID Connect Provider
            authorization: "/auth"
            token: "/token"
            userinfo: "/userinfo"
            logout: "/logout"
        display: "page"                   #How the authentication form will be display to the enduser
        scope: "email profile openid offline_access" #List of the scope you need
        authentication_ttl: 300         #Maximum age of the authentication
        token_ttl: 300                  #Maximum age for tokenID
        jwk_url: "https://login.lescommuns.org:8443/auth/realms/master/protocol/openid-connect/certs" #URL to the Json Web Key of OpenID Connect Provider
        jwk_cache_ttl: 86400             #Validity periods in second where the JWK store in cache is valid
        enabled_state: true             #Enable the use of the state value. This is useful for mitigate replay attack
        enabled_nonce: true             #Enable the use of the nonce value. This is useful for mitigate replay attack
        enduserinfo_request_method: "POST" #Define the method (POST, GET) used to request the Enduserinfo Endpoint of the OIDC Provider
        redirect_after_logout: "https://sso-synfony.marc.fun"           #URI or route name used for redirect user after a logout

    Edit vendor/waldo/openid-connect-relying-party-bundle/Waldo/OpenIdConnect/RelyingPartyBundle/OpenIdConnect/Constraint/IDTokenValidator.php and comment $this->errors[] = "Issuer are not the same"; because Keycloak return "https://login.lescommuns.org:8443/auth/realms/master" and the script is comparing to the value of the issuer in config.yml wich is "https://login.lescommuns.org:8443/auth/realms/master/protocol/openid-connect"

    To show the result of SSO, edit vendor/waldo/openid-connect-relying-party-bundle/Waldo/OpenIdConnect/RelyingPartyBundle/OpenIdConnect/ResourceOwner/AbstractGenericOICResourceOwner.php and after $oicToken->setRawTokenData($content); add :

    echo '<pre>'; print_r($content); echo '</pre>'; exit;

    The logout URL is <a href="{{ path('_oic_rp_logout') }}">Logout</a>

    Upgrading

    https://www.keycloak.org/docs/latest/release_notes/ https://www.keycloak.org/docs/latest/upgrading

    • Prepare an other instance of Keycloak (see above), including Widfly and MySQL adaptors. Do not add users.
    • Copy database (to a new one), standalone directory (update standalone.xml with new database name) and custom theme from the old Keycloak.
    • Stop the old server to avoid ports conflicts.
    • When the new server is lunched, standalone.xml file and database are - automatically updated for the new version.

    Links to explore