diff options
Diffstat (limited to 'binding/oidc-agent.c')
-rw-r--r-- | binding/oidc-agent.c | 812 |
1 files changed, 812 insertions, 0 deletions
diff --git a/binding/oidc-agent.c b/binding/oidc-agent.c new file mode 100644 index 0000000..1f09e7a --- /dev/null +++ b/binding/oidc-agent.c @@ -0,0 +1,812 @@ +/* + * Copyright (C) 2017 "IoT.bzh" + * Author: José Bollo <jose.bollo@iot.bzh> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define _GNU_SOURCE + +#include <stdlib.h> +#include <stdio.h> +#include <stdint.h> +#include <string.h> +#include <pthread.h> + +#include <json-c/json.h> + +#include "oidc-agent.h" +#include "escape.h" +#include "curl-wrap.h" + +/***************** utilities *************************/ + +static const char string_empty[] = ""; +static const char string_authorization_endpoint[] = "authorization_endpoint"; +static const char string_token_endpoint[] = "token_endpoint"; +#if 0 +static const char string_issuer[] = "issuer"; +static const char string_userinfo_endpoint[] = "userinfo_endpoint"; +static const char string_revocation_endpoint[] = "revocation_endpoint"; +static const char string_jwks_uri[] = "jwks_uri"; +#endif + +#define MAX_IDP_COUNT 20 +#define MAX_APPLI_COUNT 100 + +static struct json_object *idps; +static struct json_object *applis; + +static pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; + +/***************** utilities *************************/ + +/* + * Get the object of 'name' in the 'container' and return it. + * Creates the result if needed and add it to the container. + * When 'maxcount' isn't zero the final count will not exceed 'maxcount'. + */ +static struct json_object *j_container_item(struct json_object *container, const char *name, int maxcount) +{ + struct json_object *result; + + /* ensure object of 'name' exists */ + if (!json_object_object_get_ex(container, name, &result)) { + if (maxcount && json_object_object_length(container) >= maxcount) + return NULL; + result = json_object_new_object(); + if (!result) + return NULL; + json_object_object_add (container, name, result); + } + return result; +} + +/* + * Like 'j_container_item' but also creates the 'container' if needed. + */ +static struct json_object *j_container_item_make(struct json_object **container, const char *name, int maxcount) +{ + struct json_object *cont; + + /* ensure container exists */ + cont = *container; + if (!cont) { + cont = json_object_new_object(); + if (!cont) + return NULL; + *container = cont; + } + return j_container_item(cont, name, maxcount); +} + +/* + * Adds in 'dest' the fields of 'src' + * Also when the value of a field of 'src' is null, delete the field of 'dst' + */ +static void j_merge(struct json_object *dest, struct json_object *src) +{ + struct json_object_iter i; + json_object_object_foreachC(src, i) { + if (json_object_is_type(i.val, json_type_null)) + json_object_object_del(dest, i.key); + else + json_object_object_add(dest, i.key, json_object_get(i.val)); + } +} + +/***************** IDP **************************/ + +/* + * Set the values of 'desc' for the idp of 'name'. + * Return 0 on error or 1 on success. + */ +int oidc_idp_set(const char *name, struct json_object *desc) +{ + struct json_object *idp; + int result = 0; + + pthread_rwlock_wrlock(&rwlock); + idp = j_container_item_make(&idps, name, MAX_IDP_COUNT); + if (idp) { + j_merge(idp, desc); + result = 1; + } + pthread_rwlock_unlock(&rwlock); + return result; +} + +/* + * Return 1 if idp of 'name' exists or 0 otherwise. + */ +int oidc_idp_exists(const char *name) +{ + int result; + + pthread_rwlock_rdlock(&rwlock); + result = json_object_object_get_ex(idps, name, NULL); + pthread_rwlock_unlock(&rwlock); + + return result; +} + +/* + * Deletes the idp of 'name'. + */ +void oidc_idp_delete(const char *name) +{ + pthread_rwlock_wrlock(&rwlock); + json_object_object_del(idps, name); + pthread_rwlock_unlock(&rwlock); +} + +/***************** APPLI **************************/ + +/* + * Returns the name of the idp of the 'appli'. + * Returns NULL when appli isn't set or default idp isn't set. + */ +static const char *get_default_idp(const char *appli) +{ + struct json_object *a, *i; + + if (!json_object_object_get_ex(applis, appli, &a)) + return NULL; + if (!json_object_object_get_ex(a, string_empty, &i)) + return NULL; + return json_object_get_string(i); +} + +/* + * Returns the application data related to the 'appli' for the 'idp'. + * If 'ja' isn't null, returns in it the object for the application 'appli'. + * Returns NULL in case of error. + */ +static struct json_object *get_appli_idp(const char *appli, const char *idp, struct json_object **ja) +{ + struct json_object *a, *i; + + if (!json_object_object_get_ex(applis, appli, &a) || !json_object_object_get_ex(a, idp, &i)) + return NULL; + if (ja) + *ja = a; + return i; +} + +/* + * Set the description 'desc' for the application of 'name' and + * the 'idp'. When 'make_default' is set it, make it the default idp + * for the application. + * Return 0 on error or 1 on success. + */ +int oidc_appli_set(const char *name, const char *idp, struct json_object *desc, int make_default) +{ + struct json_object *a, *ai; + int result = 0; + + pthread_rwlock_wrlock(&rwlock); + a = j_container_item_make(&applis, name, MAX_APPLI_COUNT); + if (a) { + ai = j_container_item(a, idp, 0); + if (ai) { + j_merge(ai, desc); + if (make_default || !json_object_object_get_ex(a, string_empty, NULL)) + json_object_object_add(a, string_empty, json_object_new_string(idp)); + result = 1; + } + } + pthread_rwlock_unlock(&rwlock); + return result; +} + +/* + * Is the appli of 'name' defined? + * Return 1 if answer is yes or 0 for no. + */ +int oidc_appli_exists(const char *name) +{ + int result; + + pthread_rwlock_rdlock(&rwlock); + result = json_object_object_get_ex(applis, name, NULL); + pthread_rwlock_unlock(&rwlock); + + return result; +} + +/* + * Does the appli of 'name' has the 'idp' defined? + * Return 1 if answer is yes or 0 for no. + */ +int oidc_appli_has_idp(const char *name, const char *idp) +{ + int result; + + pthread_rwlock_rdlock(&rwlock); + result = !!get_appli_idp(name, idp, NULL); + pthread_rwlock_unlock(&rwlock); + + return result; +} + +/* + * Set 'idp' as default for the application of 'name'. + * Returns 0 on error (appli or idp for appli not existing) + * or 1 in case of success. + */ +int oidc_appli_set_default_idp(const char *name, const char *idp) +{ + struct json_object *a, *i; + + pthread_rwlock_wrlock(&rwlock); + i = get_appli_idp(name, idp, &a); + if (i) + json_object_object_add(a, string_empty, json_object_new_string(idp)); + pthread_rwlock_unlock(&rwlock); + + return !!i; +} + +/* + * Deletes the application of 'name' + */ +void oidc_appli_delete(const char *name) +{ + pthread_rwlock_wrlock(&rwlock); + json_object_object_del(applis, name); + pthread_rwlock_unlock(&rwlock); +} + +/***************** AUTHORISATION **************************/ + +/* parameters */ +enum param +{ + Param_Access_Token, + Param_Acr_Values, + Param_Authorization, + Param_Client_Id, + Param_Client_Secret, + Param_Code, + Param_Display, + Param_Error, + Param_Error_Description, + Param_Error_Uri, + Param_Expires_In, + Param_Grant_Type, + Param_Id_Token, + Param_Id_Token_Hint, + Param_Login_Hint, + Param_Max_Age, + Param_Nonce, + Param_Password, + Param_Prompt, + Param_Redirect_Uri, + Param_Refresh_Token, + Param_Response_Type, + Param_Scope, + Param_State, + Param_Token_Type, + Param_Ui_Locales, + Param_Username, + PARAM_COUNT +}; + +#if PARAM_COUNT > 30 +# error "Too much parameters" +#endif +#define PARAM(p) ((uint32_t)((uint32_t)1 << (Param_##p))) + +/* args of authorization requests */ +struct args +{ + struct json_object *appli; + struct json_object *idp; + struct json_object *args; + struct oidc_grant_cb cb; + int locked; + int refresh; + uint32_t mandatory; + uint32_t all; + struct json_object *header; + struct json_object *query; +}; + +/* Release the lock if needed */ +static void args_unlock(struct args *args) +{ + if (!args->locked) { + pthread_rwlock_unlock(&rwlock); + args->locked = 0; + } +} + +/* Release the memory needed by args */ +static void args_destroy(struct args *args) +{ + json_object_put(args->appli); + json_object_put(args->idp); + json_object_put(args->header); + json_object_put(args->query); + free(args); +} + +/* Send the success event with the gained tokens */ +static void args_send_success(struct args *args, struct json_object *result) +{ + args_unlock(args); + args->cb.success(args->cb.closure, result); + args_destroy(args); +} + +/* Sends the error event with the indice to the client of args */ +static void args_send_error(struct args *args, const char *message, const char *indice) +{ + args_unlock(args); + args->cb.error(args->cb.closure, message, indice); + args_destroy(args); +} + +/* Send the error and also return NULL */ +static inline struct args *args_send_error_null(struct args *args, const char *message, const char *indice) +{ + args_send_error(args, message, indice); + return NULL; +} + +/* creates a struct args from the arguments, returns NULL on error */ +struct args *mkargs(const char *appli, const char *idp, struct json_object *args, const struct oidc_grant_cb *cb) +{ + struct args *result; + struct json_object *obj; + + /* allocates the args */ + result = calloc(1, sizeof *result); + if (!result) { + cb->error(cb->closure, "Out of memory", NULL); + return NULL; + } + + /* init of the structure */ + result->cb = *cb; + result->args = args; + result->header = json_object_new_object(); + result->query = json_object_new_object(); + + /* lock in read */ + pthread_rwlock_rdlock(&rwlock); + result->locked = 1; + + /* check previous allocations */ + if (!result->query || !result->header) { + return args_send_error_null(result, "Out of memory", NULL); + } + + /* check whether default idp */ + if (!idp) { + idp = get_default_idp(appli); + if (!idp) + return args_send_error_null(result, "No default IDP", NULL); + } + + /* get the IDP */ + if (!json_object_object_get_ex(idps, idp, &obj)) + return args_send_error_null(result, "Unknown IDP", idp); + result->idp = json_object_get(obj); + + /* get the appli */ + obj = get_appli_idp(appli, idp, NULL); + if (!obj) + return args_send_error_null(result, "Unknown APPLI for IDP", appli); + result->appli = json_object_get(obj); + + return result; +} + +/* get a value for a struct args */ +static struct json_object *args_object(struct args *args, const char *name) +{ + struct json_object *result; + + if (!json_object_object_get_ex(args->appli, name, &result) + && !json_object_object_get_ex(args->idp, name, &result) + && !json_object_object_get_ex(args->args, name, &result)) + result = NULL; + return result; +} + +/* get a string value for a struct args */ +static const char *args_string(struct args *args, const char *name) +{ + struct json_object *object = args_object(args, name); + return object ? json_object_get_string(object) : NULL; +} + +/* add a data */ +static int args_add(struct args *args, uint32_t val, const char *name, int query) +{ + struct json_object *obj, *dest; + + if (val & args->all) { + obj = args_object(args, name); + if (obj) { + dest = query ? args->query : args->header; + json_object_object_add(dest, name, json_object_get(obj)); + } + else if (val & args->mandatory) { + args_send_error(args, "Mandatory field missing", name); + return 0; + } + } + + return 1; +} + +/* + * Makes the CURL object for the given 'url' for either GET or POST depending + * on 'post' with the added 'header' fields and the given query parameters. + * Returns NULL on error. + * Ex: + * + * curl_json("http://iot.bzh/api", 0, {"X-Index": "no"}, {"fast":true,"item":"2345-hellfest"}) + * + * produces the query: + * + * GET /api?fast=true&item=2345-hellfest HTTP/1.1 + * Host: iot.bzh + * X-Index: no + * + * while the same but with post not null produces: + * + * POST /api HTTP/1.1 + * Host: iot.bzh + * X-Index: no + * Content-Type: application/x-www-form-urlencoded + * + * fast=true&item=2345-hellfest + * + */ +static CURL *curl_json(const char *url, int post, struct json_object *header, struct json_object *query) +{ + const char **args, *str; + struct json_object_iter i; + int idx; + CURL *result; + + /* create args array */ + idx = 1 + (json_object_object_length(query) << 1); + args = malloc((unsigned)idx * sizeof *args); + if (!args) + return NULL; + + /* fill the args array */ + args[--idx] = NULL; + json_object_object_foreachC(query, i) { + str = json_object_get_string(i.val); + args[--idx] = str; + args[--idx] = i.key; + } + + /* prepare the query */ + if (post) + result = curl_wrap_prepare_post(url, NULL, args); + else + result = curl_wrap_prepare_get(url, NULL, args); + free(args); + if(!result) + return NULL; + + /* add headers */ + if (header) { + json_object_object_foreachC(header, i) { + str = json_object_get_string(i.val); + if (!curl_wrap_add_header_value(result, i.key, str)) { + curl_easy_cleanup(result); + return NULL; + } + } + } + return result; +} + +/* + * Extract from the answer of 'curl' whose 'content' has 'size' bytes the + * embeded JSON object (if any). + * Returns it or returns NULL if the answer can't be interpreted. + */ +static struct json_object *decode_perform_result(CURL *curl, const char *content, size_t size) +{ + int i; + const char **args; + struct json_object *result; + + /* is it an url encoded answer? */ + if (curl_wrap_content_type_is(curl, "application/x-www-form-urlencoded")) { + /* yes, unescape as an array of strings */ + args = unescape_args(content); + if (!args) + result = NULL; + else { + /* wrap the key=value pairs in an object */ + result = json_object_new_object(); + if (result) { + for (i = 0 ; args[i] ; i += 2) + json_object_object_add(result, args[i], + json_object_new_string(args[i+1])); + } + free(args); + } + } else if (curl_wrap_content_type_is(curl, "application/json")) { + /* interpret the json */ + result = json_tokener_parse (content); + } else { + /* by default, still try to interpret the answer as if json */ + result = json_tokener_parse (content); + } + return result; +} + +/* + * Treats the result of the query 'curl' of 'content' of 'size' bytes for the 'args' + */ +static void perform_result(struct args *args, CURL *curl, const char *content, size_t size) +{ + struct json_object *obj, *at, *tt; + char *txt; + + /* get answer */ + obj = decode_perform_result(curl, content, size); + if (!obj) + return args_send_error(args, "unable to extract answer", content); + + /* process the answer */ + if (json_object_object_get_ex(obj, "access_token", &at) && json_object_object_get_ex(obj, "token_type", &tt)) { + if (!strcmp(json_object_get_string(tt), "bearer")) { + if (asprintf(&txt, "Bearer %s", json_object_get_string(at)) > 0) { + json_object_object_add(obj, "authorization", json_object_new_string(txt)); + free(txt); + } + } + } + + /* merge the answer to the token args in case of refresh */ + if (args->refresh) + j_merge(args->args, obj); + + /* send the answer */ + args_send_success(args, obj); +} + +/* + * Treats the redirect answer of the query 'curl' of 'content' of 'size' bytes for the 'args' + */ +static void perform_redirect(struct args *args, CURL *curl, const char *content, size_t size) +{ + /* TODO: handle redirection for the normal flow */ + return args_send_error(args, "unhandled redirection", content); +} + +/* + * Handle the result of the query 'curl' of 'status'. If a data is returned, it is available in + * 'content' of 'size' bytes. + * When 'status' is 0, an error occured. Otherwise, 'statu' isn't zero. + */ +static void perform_callback(void *closure, int status, CURL *curl, const char *content, size_t size) +{ + long code; + struct args *args = closure; + + /* query error ? */ + if (!status + || curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code) != CURLE_OK) + return args_send_error(args, "query error", NULL); /* TODO: IMPROVE? */ + + /* get the returned code */ + switch (code) { + case 200: + return perform_result(args, curl, content, size); + case 302: + return perform_redirect(args, curl, content, size); + case 400: + case 401: + return args_send_error(args, content ? : "returned code error", content ? "returned code error" : content); + default: + return args_send_error(args, content ? : "unexpected code error", content ? "unexpected code error" : content); + } +} + +/* + * Main function for performing OAuth2/OpenId Connect transactions. + * The structure 'args' is filled with the needed values: + * Application data as json object, IDP data as json object + * Contextual arguments for the transaction. + * 'endpoint' must be the name of an endpoint in the context of 'args'. + * 'operation' is the value that will get either response_type or grant_type, + * depending on the nature of the required parameters 'mandatory'. + * 'mandatory' designates the mandatory parameters. + * 'optional' designate the optional parameters. + */ +static void perform(struct args *args, const char *endpoint, const char *operation, uint32_t mandatory, uint32_t optional) +{ + int post; + const char *url, *type; + CURL *curl; + + /* set the flags */ + args->mandatory = mandatory; + args->all = mandatory | optional; + + /* get the endpoint */ + url = args_string(args, endpoint); + if (!url) + return args_send_error(args, "No endpoint", endpoint); + + /* get the operation type */ + if ((mandatory & PARAM(Response_Type)) == PARAM(Response_Type)) { + type = "response_type"; + post = 0; /* can be 1 sometimes so not risk here */ + } else if ((mandatory & PARAM(Grant_Type)) == PARAM(Grant_Type)) { + type = "grant_type"; + post = 1; /* must be 1 */ + } else + return args_send_error(args, "Unexpected operation Type", NULL); + + json_object_object_add(args->query, type, json_object_new_string(operation)); + + /* get the arguments */ + if (1 + && args_add(args, PARAM(Access_Token), "access_token", 1) + && args_add(args, PARAM(Acr_Values), "acr_values", 1) + && args_add(args, PARAM(Authorization), "authorization", 0) + && args_add(args, PARAM(Client_Id), "client_id", 1) + && args_add(args, PARAM(Client_Secret), "client_secret", 1) + && args_add(args, PARAM(Code), "code", 1) + && args_add(args, PARAM(Display), "display", 1) + && args_add(args, PARAM(Expires_In), "expires_in", 1) + && args_add(args, PARAM(Id_Token_Hint), "id_token_hint", 1) + && args_add(args, PARAM(Login_Hint), "login_hint", 1) + && args_add(args, PARAM(Max_Age), "max_age", 1) + && args_add(args, PARAM(Nonce), "nonce", 1) + && args_add(args, PARAM(Password), "password", 1) + && args_add(args, PARAM(Prompt), "prompt", 1) + && args_add(args, PARAM(Redirect_Uri), "redirect_uri", 1) + && args_add(args, PARAM(Refresh_Token), "refresh_token", 1) + && args_add(args, PARAM(Scope), "scope", 1) + && args_add(args, PARAM(State), "state", 1) + && args_add(args, PARAM(Token_Type), "token_type", 1) + && args_add(args, PARAM(Ui_Locales), "ui_locales", 1) + && args_add(args, PARAM(Username), "username", 1) + ) { + /* creates the curl query */ + curl = curl_json(url, post, args->header, args->query); + if (!curl) + return args_send_error(args, "out of memory", NULL); + + /* release data */ + args_unlock(args); + + /* perform the request to the server */ + curl_wrap_do(curl, perform_callback, args); + } +} + +/* perform a grant of flow Flow_Resource_Owner_Password_Credentials_Grant */ +static void grant_owner_password(struct args *args) +{ + perform(args, string_token_endpoint, "password", + PARAM(Grant_Type) | PARAM(Username) | PARAM(Password), + PARAM(Scope) | PARAM(Authorization) + ); +} + +/* perform a grant of flow Flow_Client_Credentials_Grant */ +static void grant_client_credentials(struct args *args) +{ + perform(args, string_token_endpoint, "client_credentials", + PARAM(Grant_Type), + PARAM(Scope) | PARAM(Authorization) + ); +} + +/* switches the requests depending on 'flow' */ +static void grant(struct args *args, enum oidc_grant_flow flow) +{ + /* ensure args is valid */ + if (!args) + return; + + /* process for flow */ + switch(flow) { + + case Flow_Resource_Owner_Password_Credentials_Grant: + grant_owner_password(args); + break; + + case Flow_Client_Credentials_Grant: + grant_client_credentials(args); + break; + + case Flow_Authorization_Code_Grant: + case Flow_Implicit_Grant: + case Flow_Extension_Grant: + args_send_error(args, "Unsupported flow", NULL); + break; + + case Flow_Invalid: + default: + args_send_error(args, "Invalid flow", NULL); + break; + } +} + +/* + * Initiates a grant with the given 'flow'. + * 'appli' and 'idp' designates the appli and the idp that have been recorded. + * when idp == NULL or idp == "", the default idp of 'appli' is used. + * 'args' contains parameters expected in plus for the grant transaction. + * 'cb' describes the callback actions that are called before + * the function returns. + */ +void oidc_grant(const char *appli, const char *idp, struct json_object *args, const struct oidc_grant_cb *cb, enum oidc_grant_flow flow) +{ + grant(mkargs(appli, idp, args, cb), flow); +} + +/* + * Like oidc_grant for flow Flow_Resource_Owner_Password_Credentials_Grant + */ +void oidc_grant_owner_password(const char *appli, const char *idp, struct json_object *args, const struct oidc_grant_cb *cb) +{ + grant(mkargs(appli, idp, args, cb), Flow_Resource_Owner_Password_Credentials_Grant); +} + +/* + * Like oidc_grant for flow Flow_Client_Credentials_Grant + */ +void oidc_grant_client_credentials(const char *appli, const char *idp, struct json_object *args, const struct oidc_grant_cb *cb) +{ + grant(mkargs(appli, idp, args, cb), Flow_Client_Credentials_Grant); +} + +/* + * Refreshes the 'token' for the 'appli' and the 'idp'. + * 'cb' describes the callback actions that are called before + * the function returns. + */ +void oidc_token_refresh(const char *appli, const char *idp, struct json_object *token, const struct oidc_grant_cb *cb) +{ + struct args *args; + + args = mkargs(appli, idp, token, cb); + if (!args) + return; + args->refresh = 1; + perform(args, string_token_endpoint, "refresh_token", + PARAM(Grant_Type) | PARAM(Refresh_Token), + PARAM(Scope) | PARAM(Authorization) + ); +} + +/* + * Adds the header "authorisation" with the bearer access_token of 'token'. + * Return 1 on case of success or 0 otherwise. + */ +int oidc_add_bearer(CURL *curl, struct json_object *token) +{ + struct json_object *bearer; + + return json_object_object_get_ex(token, "authorization", &bearer) + && curl_wrap_add_header_value(curl, "authorization", json_object_get_string(bearer)); +} + + |