Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Guidance needed for setting up PHPIPAM with Keycloak for SAML2 Authentication #3860

Closed
JonTheNiceGuy opened this issue May 31, 2023 · 6 comments

Comments

@JonTheNiceGuy
Copy link
Contributor

JonTheNiceGuy commented May 31, 2023

I have a Keycloak server and a recently installed phpipam server. I would like to use the Keycloak server to provide SSO to PHPIPAM. I've tried several routes to get it working, and none of them have, so far! Could someone help out with what, if anything, I'm doing wrong? :)

In Keycloak I created a new client like this:
image

So, is this the right endpoint address: https://<SOME.PHPIPAM.SERVER/saml2/?

If I don't specify the client ID as urn:<somekey>:<somekey> it doesn't render in the list of SSO options later. I've noticed other screenshots (in particular, the one from @GaryAllan in a response in issue 3013 shows Https://phpipam.*******.com so I'm not sure if this is a quirk of my keycloak or if there's something else wacky going on!

Clicking save on that takes me to this page:
image

I've set the Valid Redirect URLs to https://phpipam.example.org/* and I've made a note of the SAML signing keys (certificate and key) from the SAML keys tab.

Having done all that, I then went into PHPIPAM, and created a new authentication source;

image

I set the client ID to the same as I created in Keycloak (urn:phpipam:phpipam), and am aware of the SAML signing keys, however I've guessed at the IDP provider fields and am not at all clear on where to get the IDP public cert, especially as it's been signed by LetsEncrypt - so do we need to get this? The key I would have used is wrapped in -----BEGIN/END CERTIFICATE----- but that doesn't feel right!

I do have pretty url mode switched on, so we can do strict mode#3859

I'm sorry if it feels like I'm asking for a lot here, but I'm just completely stumped on these values, and the previous tools I've used to create Keycloak's SAML connections have all had a file provided (e.g. https://signin.aws.amazon.com/static/saml-metadata.xml) that naturally makes life a lot easier!

@SamuelePilleri
Copy link

SamuelePilleri commented Jun 1, 2023

I'm running a newer version of Keycloak, so your mileage might vary, but this it how I did it.

When creating a client, just give it a name and an ID.

IDP issuer is: https://<your.keycloak.address>/realms/<your_realm>
IDP login url and logout url are both: https://<your.keycloak.address>/realms/<your_realm>/protocol/saml

Then on Keycloak go to Realm settings and click on SAML 2.0 Identity Provider Metadata. It will open an XML file. There you will find the IDP X.509 public cert of your Keycloak instance under X509Certificate. Copy it to phpipam.

Enable Sign Authn requests.

Now, you will need to generate a key for phpipam. The easiest way is from Keycloak, go to Clients > select your client > Keys (tab) and hit Regenerate: the key in the text field is the Authn X.509 signing cert, the file Keycloak prompts you to download is the Authn X.509 signing cert key. Copy both to phpipam.

Finally, from you client settings tab in Keycloak add https://<your.phpipam.address>/saml2/ under Valid redirect URIs.

You should be good to go. I'm using HTTPS because I have certificates, but they may not be necessary.

@JonTheNiceGuy
Copy link
Contributor Author

Hi! Thanks @SamuelePilleri for your advice! It really helped me move forwards!

I had two other tweaks I had to make, one was where I had to add a role mapper from Keycloak, and I also had to disable JIT, as I couldn't figure out how to get keycloak to send the display name. Any hints on this element?

You were right, by the way, Samuele - the version of keycloak is pretty old - I've just not got around to upgrading that yet!!

@SamuelePilleri
Copy link

Attributes such as display name should be a claim in IdM jargon. The doc states the IdP (ie. Keycloak) should send a display_name claim and looking at the code confirms so. I think that can be done with a role mapper, but I haven't tried this part. Also, looking online I haven't found references to display_name, but some point to displayName which may be what Keycloak sends by default.

Hope this helps, let us know if you manage to get it working! 💪

@JonTheNiceGuy
Copy link
Contributor Author

Right, I sorted that part!

To create your display_name object, go into the "mappers" tab on Keycloak and create a new "Javascript Mapper".
image

  • Name: display_name
  • Mapper Type: Javascript Mapper
  • Script: user.getFirstName() + ' ' + user.getLastName()
  • Single Value Attribute: On
  • Friendly Name: display_name
  • SAML Attribute Name: display_name
  • SAML Attribute NameFormat: Basic

And it works. I was then informed that I'd not sent the "email" through, so I created a new "User Attribute" with the following values:

  • Name: email
  • Mapper Type: User Attribute
  • User Attribute: email
  • Friendly Name: email
  • SAML Attribute Name: email
  • SAML Attribute NameFormat: Basic
  • Aggregate attribute values: Off

This doesn't send through groups, but I got to the bottom of that as well!

This is a bit more tricky, as you need to do a bit more wrangling of the Javascript Mapper, but in this case I created a Javascript Mapper with the following attributes (I'll leave "Script" to the end):

  • Name: groups
  • Mapper Type: Javascript Mapper
  • Single Value Attribute: On
  • Friendly Name: groups
  • SAML Attribute Name: groups
  • SAML Attribute NameFormat: Basic

The Script has more to do!

everyone_who_can_access_gets_read_only_access = false;

send_groups = "";
var GroupSet = user.getGroups();
for each (var group in GroupSet) {
    use_group = ""
    switch (group.getName()) {
        case "LDAP_GROUP_NAME_1":
            use_group = "PHPIPAM_GROUP_NAME_1";
            break;
        case "LDAP_GROUP_NAME_2":
            use_group = "PHPIPAM_GROUP_NAME_2";
            break;
    }
    if (use_group != "") {
        if (send_groups != "") {
          send_groups = send_groups + ","
        }
        send_groups = send_groups + use_group;
    }    
}

if (send_groups === "" && everyone_who_can_access_gets_read_only_access) {
    "Guests"
} else {
    send_groups
}

To get the value is_admin (to promote the user to administrator), then you could do something similar:

  • Name: is_admin
  • Mapper Type: Javascript Mapper
  • Single Value Attribute: On
  • Friendly Name: is_admin
  • SAML Attribute Name: is_admin
  • SAML Attribute NameFormat: Basic

Script:

is_admin = false;
var GroupSet = user.getGroups();
for each (var group in GroupSet) {
    use_group = ""
    switch (group.getName()) {
        case "phpipamadmins":
            is_admin = true;
            break;
    }
}
is_admin

The only part I didn't bother with is modules, but that's a bit outside the area I'm experimenting with right now!

Thanks for your help!

@JonTheNiceGuy
Copy link
Contributor Author

Thanks to everyone involved in PHPIPAM and in particular @SamuelePilleri. I wrote this up in a blog post. I hope it's useful!

My post: IP Address Management using PHPIPAM integrated with Keycloak for SAML2 Authentication

@JonTheNiceGuy
Copy link
Contributor Author

JonTheNiceGuy commented Jun 7, 2023

Here's one way to deliver modules!

// Current modules as at 2023-06-07
// Some default values are set here.
// -1 = Unset value
// 0  = No access
// 1  = Read-only
// 2  = Amend existing records
// 3  = Delete and Create records
noaccess       =  0;
readonly       =  1;
readwrite      =  2;
readwriteadmin =  3;
unsetperm      = -1;

var modules = {
    "*":        readonly, "vlan":     unsetperm, "l2dom":     unsetperm, "devices":   unsetperm,
    "racks":   unsetperm, "circuits": unsetperm, "nat":       unsetperm, "locations":  noaccess,
    "routing": unsetperm, "pdns":     unsetperm, "customers": unsetperm,
}

function updateModules(modules, new_value, list_of_modules) {
    for (var module in list_of_modules) {
        modules[module] = new_value;
    }
    return modules;
}

var GroupSet = user.getGroups();
for (var group in GroupSet) {
    switch (group.getName()) {
        case "LDAP_ROLE_1":
            modules = updateModules(modules, readwriteadmin, [
                'racks', 'devices', 'nat', 'routing'
            ]);
            break;
    }
}

var moduleList = '';

for (var key in modules) {
    if (modules.hasOwnProperty(key) && modules[key] !==-1) {
        if (moduleList !== '') {
            moduleList += ',';
        }
        moduleList += key + ':' + modules[key];
    }
}

moduleList;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants