Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions src/man/sssd-idp.5.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@
<listitem>
<para>
Required option that specifies the IdP product.
Currently Entra ID (entra_id) and Keycloak
(keycloak) are supported.
Currently Entra ID (entra_id), Keycloak
(keycloak), and Authentik (authentik) are supported.
</para>
<para>
Depending on the IdP product additional platform
Expand Down Expand Up @@ -276,7 +276,7 @@
id_provider = idp
idp_type = entra_id
idp_client_id = 12345678-abcd-0101-efef-ba9876543210
idp_client_secret = YOUR-CLIENT-SCERET
idp_client_secret = YOUR-CLIENT-SECRET
idp_token_endpoint = https://login.microsoftonline.com/TENNANT-ID/oauth2/v2.0/token
idp_userinfo_endpoint = https://graph.microsoft.com/v1.0/me
idp_device_auth_endpoint = https://login.microsoftonline.com/TENNANT-ID/oauth2/v2.0/devicecode
Expand All @@ -288,13 +288,26 @@ idp_auth_scope = openid profile email
idp_type = keycloak:https://master.keycloak.test:8443/auth/admin/realms/master/
id_provider = idp
idp_client_id = myclient
idp_client_secret = YOUR-CLIENT-SCERET
idp_client_secret = YOUR-CLIENT-SECRET
idp_token_endpoint = https://master.keycloak.test:8443/auth/realms/master/protocol/openid-connect/token
idp_userinfo_endpoint = https://master.keycloak.test:8443/auth/realms/master/protocol/openid-connect/userinfo
idp_device_auth_endpoint = https://master.keycloak.test:8443/auth/realms/master/protocol/openid-connect/auth/device
idp_id_scope = profile
idp_auth_scope = openid profile email
</programlisting>
<programlisting>
[domain/authentik]
id_provider = idp
idp_type = authentik:https://authentik.company/api/v3/core/
idp_client_id = myclient
idp_client_secret = YOUR-CLIENT-SECRET
idp_token_endpoint = https://authentik.company/application/o/token/
idp_userinfo_endpoint = https://authentik.company/application/o/userinfo/
idp_device_auth_endpoint = https://authentik.company/application/o/device/
idp_id_scope = goauthentik.io/api
idp_auth_scope = openid profile email offline_access
</programlisting>

</para>
</refsect1>

Expand Down
33 changes: 22 additions & 11 deletions src/oidc_child/oidc_child.c
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,8 @@ int main(int argc, const char *argv[])
int exit_status = EXIT_FAILURE;
char *out = NULL;
char *client_secret_tmp = NULL;
json_t *tmp_json;
char *tmp_str;

ret = parse_cli(argc, argv, &opts);
if (ret != EOK) {
Expand Down Expand Up @@ -864,18 +866,27 @@ int main(int argc, const char *argv[])
/* Currently this reply is used by ipa-otpd as RADIUS Proxy-State and
* Reply-Message.
*/
fprintf(stdout,
"{\"device_code\":\"%s\",\"expires_in\":%d,\"interval\":%d}\n",
dc_ctx->device_code, dc_ctx->expires_in, dc_ctx->interval);
fprintf(stdout,
"oauth2 {\"verification_uri\": \"%s\", "
"\"user_code\": \"%s%s%s\"}\n",
dc_ctx->verification_uri, dc_ctx->user_code,
dc_ctx->verification_uri_complete == NULL ? ""
: "\", \"verification_uri_complete\": \"",
dc_ctx->verification_uri_complete == NULL ? ""
: dc_ctx->verification_uri_complete);
tmp_json = json_pack("{s:s, s:i, s:i}",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi,

thank you for adding proper JSON support here. I would suggest to move the generation of the two output line to oidc_child_json.c so that here only a single fprintf() has to be called.

bye,
Sumit

"device_code", dc_ctx->device_code,
"expires_in", dc_ctx->expires_in,
"interval", dc_ctx->interval);
tmp_str = json_dumps(tmp_json, JSON_COMPACT);
json_decref(tmp_json);
fprintf(stdout, "%s\n", tmp_str);
free(tmp_str);

tmp_json = json_pack("{s:s, s:s}",
Comment thread
alexey-tikhonov marked this conversation as resolved.
"verification_uri", dc_ctx->verification_uri,
"user_code", dc_ctx->user_code);
if (dc_ctx->verification_uri_complete != NULL) {
json_object_set_new(tmp_json, "verification_uri_complete",
json_string(dc_ctx->verification_uri_complete));
}
tmp_str = json_dumps(tmp_json, JSON_COMPACT);
Comment thread
alexey-tikhonov marked this conversation as resolved.
json_decref(tmp_json);
fprintf(stdout, "oauth2 %s\n", tmp_str);
fflush(stdout);
free(tmp_str);
}

if (opts.oidc_cmd == GET_ACCESS_TOKEN
Expand Down
180 changes: 179 additions & 1 deletion src/oidc_child/oidc_child_id.c
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,180 @@
return ret;
}

/* The following function will lookup users and groups based on Authentik's
* REST API as described in
* https://api.goauthentik.io/ */
errno_t authentik_lookup(TALLOC_CTX *mem_ctx, enum oidc_cmd oidc_cmd,
Comment thread
alexey-tikhonov marked this conversation as resolved.
Dismissed
Comment thread
sumit-bose marked this conversation as resolved.
char *base_url,
char *input, enum search_str_type input_type,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

input_type parameter accepted but never used.

(from sssd-bot' review)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment in line 471

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi,

yes, basically input_type is a placeholder for lookups by id which might be available in future. If you want to avoid this warning you can add something like e.g.

    if (input_type ==TYPE_OBJECT_ID) {
        ret = ENOTSUP;
        goto done;
    }

bye,
Sumit

bool libcurl_debug, const char *ca_db,
const char *client_id, const char *client_secret,
const char *token_endpoint, const char *scope,
const char *bearer_token, struct rest_ctx *rest_ctx,
char **out)
{
errno_t ret;
char *uri;
char *filter;
char *input_enc;
const char *obj_id;
char *sep;
char *tmp;
int page_count;
struct name_and_type_identifier authentik_name_and_type_identifier = {
.user_identifier_attr = "username",
.group_identifier_attr = "name",
.user_name_attr = "username",
.group_name_attr = "name" };

if (base_url == NULL) {
DEBUG(SSSDBG_OP_FAILURE, "Missing base URL in IdP type [authentik].\n");
return EINVAL;
}

input_enc = url_encode_string(rest_ctx, input);
if (input_enc == NULL) {
DEBUG(SSSDBG_OP_FAILURE, "Failed to encode input [%s].\n", input);
return EINVAL;
}

switch (oidc_cmd) {
case GET_USER:
case GET_USER_GROUPS:
filter = talloc_asprintf(rest_ctx, "username=%s", input_enc);
break;
case GET_GROUP:
case GET_GROUP_MEMBERS:
sep = strrchr(input, '@');
if (sep == NULL || sep == input) {
filter = talloc_asprintf(rest_ctx, "include_users=true&name=%s", input_enc);
} else {
filter = talloc_asprintf(rest_ctx, "include_users=true&search=%s", input_enc);
}
break;
default:
DEBUG(SSSDBG_OP_FAILURE, "Unknown command [%d].\n", oidc_cmd);
ret = EINVAL;
goto done;
}

if (filter == NULL) {
DEBUG(SSSDBG_OP_FAILURE, "Failed to create user search filter.\n");
ret = ENOMEM;
goto done;
}

switch (oidc_cmd) {
case GET_USER:
case GET_USER_GROUPS:
uri = talloc_asprintf(rest_ctx, "%s/users/?%s" ,base_url, filter);
break;
case GET_GROUP:
case GET_GROUP_MEMBERS:
uri = talloc_asprintf(rest_ctx, "%s/groups/?%s" ,base_url, filter);
break;
default:
DEBUG(SSSDBG_OP_FAILURE, "Unknown command [%d].\n", oidc_cmd);
ret = EINVAL;
goto done;
}

if (uri == NULL) {
DEBUG(SSSDBG_OP_FAILURE, "Failed to generate lookup URI.\n");
ret = ENOMEM;
goto done;
}

clean_http_data(rest_ctx);
ret = do_http_request(rest_ctx, uri, NULL, bearer_token);
if (ret != EOK) {
DEBUG(SSSDBG_OP_FAILURE, "User search request failed.\n");
goto done;
}

if (oidc_cmd == GET_USER || oidc_cmd == GET_GROUP) {
ret = EOK;
goto done;
}

obj_id = get_str_attr_from_embed_json_string(
rest_ctx, get_http_data(rest_ctx), "results", "pk");

if (obj_id == NULL) {
DEBUG(SSSDBG_OP_FAILURE, "Failed to read mandatory object id.\n");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi,

please read uid for users and pk for groups here and store the values as id in out. This way you cam remove the changes in idp_id_eval.c. It is expected that out contains besides posixUsername etc which are added inadd_posix_to_json() the id attribute which a unique identifier for the user or group object. Since this unique attribute is provider specific it is best to set it here instead of figuring it out in the backend.

Additionally, since you need the user's pk for the group-memberships I would suggest to create a get_str_attr_from_embed_json_int() to avoid the fallback in get_json_string().

bye,
Sumit

ret = EINVAL;
goto done;
}

switch (oidc_cmd) {
case GET_USER_GROUPS:
free(uri);
uri = talloc_asprintf(rest_ctx,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need either to use tmp context and free it at function exit, or free 'uri' explicitly here before assigning new memory allocation.
Otherwise it will hang on 'rest_ctx'.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe so

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated this only for the Authentik for now - in d75f932

"%s/groups/?include_users=false&members_by_pk=%s&page=1&page_size=100",
base_url, obj_id);
break;
case GET_GROUP_MEMBERS:
uri = talloc_asprintf(rest_ctx,
"%s/users/?groups_by_pk=%s&include_groups=false&include_roles=false&page=1&page_size=100",
base_url, obj_id);
break;
default:
DEBUG(SSSDBG_OP_FAILURE, "Unknown command [%d].\n", oidc_cmd);
ret = EINVAL;
goto done;
}

if (uri == NULL) {
DEBUG(SSSDBG_OP_FAILURE, "Failed to generate lookup URI.\n");
ret = ENOMEM;
goto done;
}

clean_http_data(rest_ctx);
switch (oidc_cmd) {
case GET_USER_GROUPS:
case GET_GROUP_MEMBERS:
ret = do_http_request_json_data(rest_ctx, uri, NULL, bearer_token);
break;
default:
DEBUG(SSSDBG_OP_FAILURE, "Unknown command [%d].\n", oidc_cmd);
ret = EINVAL;
goto done;
}

if (ret != EOK) {
DEBUG(SSSDBG_OP_FAILURE, "Member(of) search request failed.\n");
goto done;
}

ret = EOK;

done:
if (ret == EOK && out != NULL) {
page_count = get_authentik_pagination(get_http_data(rest_ctx));
if (page_count > 1) {
DEBUG(SSSDBG_MINOR_FAILURE, "WARNING: Authentik results have been "
"truncated. User or Group information might not be complete.\n");
}

tmp = get_json_string_array_from_json_string(
mem_ctx, get_http_data(rest_ctx), "results");
if (tmp == NULL) {
DEBUG(SSSDBG_OP_FAILURE, "Failed to copy output data.\n");
return ENOMEM;
}
ret = add_posix_to_json_string_array(mem_ctx,
&authentik_name_and_type_identifier,
0, tmp, out);
if (ret != EOK) {
DEBUG(SSSDBG_OP_FAILURE, "Failed to add POSIX data.\n");
}
}

return ret;
}

errno_t oidc_get_id(TALLOC_CTX *mem_ctx, enum oidc_cmd oidc_cmd,

Check warning on line 641 in src/oidc_child/oidc_child_id.c

View workflow job for this annotation

GitHub Actions / cppcheck

The function 'oidc_get_id' is never used.
char *idp_type,
char *input, enum search_str_type input_type,
bool libcurl_debug, const char *ca_db,
Expand Down Expand Up @@ -521,11 +694,16 @@
goto done;
}

if (idp_type != NULL && strncasecmp(idp_type, "keycloak:",9) == 0) {
if (idp_type != NULL && strncasecmp(idp_type, "keycloak:", 9) == 0) {
ret = keycloak_lookup(mem_ctx, oidc_cmd, base_url, input, input_type,
libcurl_debug, ca_db, client_id, client_secret,
token_endpoint, scope, bearer_token, rest_ctx,
out);
} else if (idp_type != NULL && strncasecmp(idp_type, "authentik:", 10) == 0) {
ret = authentik_lookup(mem_ctx, oidc_cmd, base_url, input, input_type,
libcurl_debug, ca_db, client_id, client_secret,
token_endpoint, scope, bearer_token, rest_ctx,
out);
} else if (idp_type == NULL
|| strcasecmp(idp_type, "entra_id") == 0
|| strncasecmp(idp_type, "entra_id:", 9) == 0) {
Expand Down
42 changes: 38 additions & 4 deletions src/oidc_child/oidc_child_json.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,19 @@ static char *get_json_string(TALLOC_CTX *mem_ctx, const json_t *root,

tmp = json_object_get(root, attr);
if (!json_is_string(tmp)) {
if (json_is_integer(tmp)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If Opus' review was wrong and this fallback is not for pk field, then what is this for?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Authentik API is annoyingly inconsistent with this field.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But where do you parse 'pk' for user objects?
store_json_user() checks for "uid"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'pk' for both, users and groups, is used in the lookup here: https://github.com/SSSD/sssd/pull/8619/changes#diff-b444f2dc72997d5070d3f385fe295aeb12ffb0e67b34a755f7c5e5c14fcd3a91R575-R579

It significantly reduces the response size of Authentik for lookups.
The API only accepts an integer PK for the user lookup, so we cannot use the UUID for that lookup.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code issues a search for members_by_pk.
But where does it reads 'pk' field for user objects?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, for group objects - in store_json_group() - you added:

    if (!json_is_string(uuid)) {
        uuid = json_object_get(group, "pk");
    }

But for user objects - in store_json_user() - you added:

    if (!json_is_string(uuid)) {
        uuid = json_object_get(user, "uid");
    }

-- no fallback to 'pk'

char buffer[64];
json_int_t i_val = json_integer_value(tmp);
snprintf(buffer, sizeof(buffer), "%" JSON_INTEGER_FORMAT, i_val);
str = talloc_strdup(mem_ctx, buffer);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could use single talloc_asprintf() instead.
But so far I don't understand why this fallback is needed at all.

if (str == NULL) {
DEBUG(SSSDBG_OP_FAILURE, "Failed to copy '%s' string.\n", attr);
return NULL;
}
return str;
}
DEBUG(SSSDBG_OP_FAILURE,
"Result does not contain the '%s' string.\n", attr);
"Result does not contain the field '%s'.\n", attr);
return NULL;
}

Expand Down Expand Up @@ -532,13 +543,16 @@ const char *get_user_identifier(TALLOC_CTX *mem_ctx, json_t *userinfo,
{
json_t *id_object = NULL;
const char *user_identifier = NULL;
const char *id_attr_list[] = { "sub", "id", NULL };
const char *id_attr_list[4];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi,

what is the purpose of this change? I'm asking because it is changing the behavior of this function. If user_identifier_attr is given the function should only be successful if user_identifier_attr is present, there should be no fall back. Only if user_identifier_attr is not set sub and id should be checked.

bye,
Sumit

int id_attr_index = 0;
size_t c;

if (user_identifier_attr != NULL) {
id_attr_list[0] = user_identifier_attr;
id_attr_list[1] = NULL;
id_attr_list[id_attr_index++] = user_identifier_attr;
}
id_attr_list[id_attr_index++] = "sub";
id_attr_list[id_attr_index++] = "id";
id_attr_list[id_attr_index] = NULL;

Comment thread
alexey-tikhonov marked this conversation as resolved.
for (c = 0; id_attr_list[c] != NULL; c++) {
id_object = json_object_get(userinfo, id_attr_list[c]);
Expand Down Expand Up @@ -600,6 +614,26 @@ const char *get_str_attr_from_json_string(TALLOC_CTX *mem_ctx,
return attr;
}

int get_authentik_pagination(const char *json_str)
{
json_error_t json_error;
json_t *result = NULL;
json_t *pagination_data = NULL;
int page_count;

result = json_loads(json_str, 0, &json_error);
if (result == NULL) {
DEBUG(SSSDBG_OP_FAILURE,
"Failed to parse json data on line [%d]: [%s].\n",
json_error.line, json_error.text);
return ENOMEM;
}
pagination_data = json_object_get(result, "pagination");
page_count = get_json_integer(pagination_data, "total_pages", true);
json_decref(result);
return page_count;
Comment thread
alexey-tikhonov marked this conversation as resolved.
}

const char *get_str_attr_from_json_array_string(TALLOC_CTX *mem_ctx,
const char *json_str,
const char *attr_name)
Expand Down
2 changes: 2 additions & 0 deletions src/oidc_child/oidc_child_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ json_t *token_data_to_json(struct devicecode_ctx *dc_ctx);
char *get_jwt(struct rest_ctx *rest_ctx, const char *token_endpoint,
const char *client_id);

int get_authentik_pagination(const char *json_str);

/* oidc_child_id.c */
errno_t oidc_get_id(TALLOC_CTX *mem_ctx, enum oidc_cmd oidc_cmd,
char *idp_type,
Expand Down
10 changes: 8 additions & 2 deletions src/providers/idp/idp_id_eval.c
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,12 @@ static errno_t store_json_user(struct idp_id_ctx *idp_id_ctx, json_t *user,
}

uuid = json_object_get(user, "id");
if (!json_is_string(uuid)) {
uuid = json_object_get(user, "uid");
}
Comment thread
Maddosaurus marked this conversation as resolved.
Comment thread
Maddosaurus marked this conversation as resolved.
if (!json_is_string(uuid)) {
DEBUG(SSSDBG_OP_FAILURE,
"JSON user object does not contain 'id' string.\n");
"JSON user object does not contain 'id' or 'uid' string.\n");
ret = EINVAL;
goto done;
}
Expand Down Expand Up @@ -163,9 +166,12 @@ static errno_t store_json_group(struct idp_id_ctx *idp_id_ctx, json_t *group,
}

uuid = json_object_get(group, "id");
if (!json_is_string(uuid)) {
uuid = json_object_get(group, "pk");
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will go out of control very soon.
If SSSD is going to support very different IdP providers with different attribute names then there should be name maps (as LDAP based providers have).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand your point. Given the time this PR has been open, and the fact that I'm not very familiar with the codebase (yet), I'd be glad if we could not implement the name map in the context of this PR.
Let's address this in a follow up PR, either by me or a maintainer. How about that?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure. Maybe we could have a dedicated feature branch.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, we can do that. I want to get this current PR wrapped up soon, as it has been open for 2 months now.
I'd really like to land the current changes in SSSD and address improvements either in a feature branch or a follow up PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi,

there should be no changes to the general backend code. Please see my comments about setting id in authentik_lookup().

bye,
Sumit

if (!json_is_string(uuid)) {
DEBUG(SSSDBG_OP_FAILURE,
"JSON group object does not contain 'id' string.\n");
"JSON group object does not contain 'id' or 'pk' string.\n");
ret = EINVAL;
goto done;
}
Expand Down
Loading