├── .formatter.exs ├── .gitignore ├── LICENSE ├── README.md ├── config ├── config.exs └── runtime.exs ├── lib └── kinde_sdk │ ├── api │ ├── o_auth.ex │ ├── organizations.ex │ └── users.ex │ ├── connection.ex │ ├── deserializer.ex │ ├── kinde_cache.ex │ ├── kinde_client_sdk.ex │ ├── model │ ├── add_organization_users_200_response.ex │ ├── add_organization_users_request.ex │ ├── create_organization_request.ex │ ├── create_user_200_response.ex │ ├── create_user_request.ex │ ├── create_user_request_identities_inner.ex │ ├── create_user_request_identities_inner_details.ex │ ├── create_user_request_profile.ex │ ├── organization.ex │ ├── organization_user.ex │ ├── remove_organization_users_200_response.ex │ ├── remove_organization_users_request.ex │ ├── update_user_request.ex │ ├── user.ex │ ├── user_identity.ex │ ├── user_identity_result.ex │ ├── user_profile.ex │ └── user_profile_v2.ex │ ├── request_builder.ex │ └── sdk │ ├── authorization_code.ex │ ├── client_credentials.ex │ ├── pkce.ex │ └── utils.ex ├── mix.exs ├── mix.lock └── test ├── authorization_code_test.exs ├── client_credentials_test.exs ├── client_test_helper.exs ├── kinde_client_sdk_test.exs ├── pkce_test.exs ├── test_helper.exs └── utils_test.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"], 3 | subdirectories: ["priv/*/migrations"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build/ 2 | /deps/ 3 | .env -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Kinde 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kinde Elixir 2 | 3 | The Kinde SDK for Elixir. 4 | 5 | You can also use the Elixir starter kit [here](https://github.com/kinde-starter-kits/elixir-starter-kit). 6 | 7 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://makeapullrequest.com) [![Kinde Docs](https://img.shields.io/badge/Kinde-Docs-eee?style=flat-square)](https://kinde.com/docs/developer-tools) [![Kinde Community](https://img.shields.io/badge/Kinde-Community-eee?style=flat-square)](https://thekindecommunity.slack.com) 8 | 9 | ### Initial set up 10 | 11 | 1. Clone the repository to your machine: 12 | 13 | ```bash 14 | git clone https://github.com/kinde-oss/kinde-elixir-sdk.git 15 | ``` 16 | 17 | 2. Go into the project: 18 | 19 | ```bash 20 | cd kinde-elixir-sdk 21 | ``` 22 | 23 | 3. Install the dependencies: 24 | 25 | ```bash 26 | mix deps.get 27 | ``` 28 | 29 | ## Documentation 30 | 31 | For details on integrating this SDK into your project, head over to the [Kinde docs](https://kinde.com/docs/) and see the [Elixir SDK](https://kinde.com/docs/developer-tools/elixir-sdk/) doc 👍🏼. 32 | 33 | ## Publishing 34 | 35 | The core team handles publishing. 36 | ## Contributing 37 | 38 | Please refer to Kinde’s [contributing guidelines](https://github.com/kinde-oss/.github/blob/489e2ca9c3307c2b2e098a885e22f2239116394a/CONTRIBUTING.md). 39 | 40 | ## License 41 | 42 | By contributing to Kinde, you agree that your contributions will be licensed under its MIT License. 43 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | 7 | # General application configuration 8 | import Config 9 | 10 | # Import environment specific config. This must remain at the bottom 11 | # of this file so it overrides the configuration defined above. 12 | # 13 | # import_config "#{config_env()}.exs" 14 | -------------------------------------------------------------------------------- /config/runtime.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # config/runtime.exs is executed for all environments, including 4 | # during releases. It is executed after compilation and before the 5 | # system starts, so it is typically used to load production configuration 6 | # and secrets from environment variables or elsewhere. Do not define 7 | # any compile-time configuration in here, as it won't be applied. 8 | # The block below contains prod specific runtime configuration. 9 | import Envar 10 | Envar.load(".env") 11 | 12 | config :kinde_sdk, base_url: Envar.get("KINDE_BASE_URL") 13 | config :kinde_sdk, domain: Envar.get("KINDE_DOMAIN") 14 | config :kinde_sdk, redirect_url: Envar.get("KINDE_REDIRECT_URL") 15 | config :kinde_sdk, backend_client_id: Envar.get("KINDE_BACKEND_CLIENT_ID") 16 | config :kinde_sdk, client_secret: Envar.get("KINDE_CLIENT_SECRET") 17 | config :kinde_sdk, logout_redirect_url: Envar.get("KINDE_LOGOUT_REDIRECT_URL") 18 | config :kinde_sdk, pkce_callback_url: Envar.get("KINDE_PKCE_REDIRECT_URL") 19 | config :kinde_sdk, pkce_logout_url: Envar.get("KINDE_PKCE_LOGOUT_URL") 20 | config :kinde_sdk, frontend_client_id: Envar.get("KINDE_FRONTEND_CLIENT_ID") 21 | 22 | if base_url = System.get_env("KINDE_SDK_BASE_URI") do 23 | config :kinde_sdk, base_url: base_url 24 | end 25 | -------------------------------------------------------------------------------- /lib/kinde_sdk/api/o_auth.ex: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator 6.3.0 (https://openapi-generator.tech). 2 | # Do not edit this file manually. 3 | 4 | defmodule KindeSDK.Api.OAuth do 5 | @moduledoc """ 6 | API calls for all endpoints tagged `OAuth`. 7 | """ 8 | 9 | alias KindeSDK.Connection 10 | import KindeSDK.RequestBuilder 11 | 12 | @doc """ 13 | Returns the details of the currently logged in user 14 | Contains the id, names and email of the currently logged in user 15 | 16 | ### Parameters 17 | 18 | - `connection` (KindeSDK.Connection): Connection to server 19 | - `opts` (keyword): Optional parameters 20 | 21 | ### Returns 22 | 23 | - `{:ok, KindeSDK.Model.UserProfile.t}` on success 24 | - `{:error, Tesla.Env.t}` on failure 25 | """ 26 | @spec get_user(Tesla.Env.client(), keyword()) :: 27 | {:ok, nil} | {:ok, KindeSDK.Model.UserProfile.t()} | {:error, Tesla.Env.t()} 28 | def get_user(connection, _opts \\ []) do 29 | request = 30 | %{} 31 | |> method(:get) 32 | |> url("/oauth2/user_profile") 33 | |> Enum.into([]) 34 | 35 | connection 36 | |> Connection.request(request) 37 | |> evaluate_response([ 38 | {200, %KindeSDK.Model.UserProfile{}}, 39 | {403, false} 40 | ]) 41 | end 42 | 43 | @doc """ 44 | Returns the details of the currently logged in user 45 | Contains the id, names and email of the currently logged in user 46 | 47 | ### Parameters 48 | 49 | - `connection` (KindeSDK.Connection): Connection to server 50 | - `opts` (keyword): Optional parameters 51 | 52 | ### Returns 53 | 54 | - `{:ok, KindeSDK.Model.UserProfileV2.t}` on success 55 | - `{:error, Tesla.Env.t}` on failure 56 | """ 57 | @spec get_user_profile_v2(Tesla.Env.client(), keyword()) :: 58 | {:ok, KindeSDK.Model.UserProfileV2.t()} | {:ok, nil} | {:error, Tesla.Env.t()} 59 | def get_user_profile_v2(connection, _opts \\ []) do 60 | request = 61 | %{} 62 | |> method(:get) 63 | |> url("/oauth2/v2/user_profile") 64 | |> Enum.into([]) 65 | 66 | connection 67 | |> Connection.request(request) 68 | |> evaluate_response([ 69 | {200, %KindeSDK.Model.UserProfileV2{}}, 70 | {403, false} 71 | ]) 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/kinde_sdk/api/organizations.ex: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator 6.3.0 (https://openapi-generator.tech). 2 | # Do not edit this file manually. 3 | 4 | defmodule KindeSDK.Api.Organizations do 5 | @moduledoc """ 6 | API calls for all endpoints tagged `Organizations`. 7 | """ 8 | 9 | alias KindeSDK.Connection 10 | import KindeSDK.RequestBuilder 11 | 12 | @doc """ 13 | Assign Users to an Organization 14 | Add existing users to an organization. 15 | 16 | ### Parameters 17 | 18 | - `connection` (KindeSDK.Connection): Connection to server 19 | - `opts` (keyword): Optional parameters 20 | - `:code` (String.t): The organization's code. 21 | - `:body` (AddOrganizationUsersRequest): 22 | 23 | ### Returns 24 | 25 | - `{:ok, KindeSDK.Model.AddOrganizationUsers200Response.t}` on success 26 | - `{:error, Tesla.Env.t}` on failure 27 | """ 28 | @spec add_organization_users(Tesla.Env.client(), keyword()) :: 29 | {:ok, nil} 30 | | {:ok, KindeSDK.Model.AddOrganizationUsers200Response.t()} 31 | | {:error, Tesla.Env.t()} 32 | def add_organization_users(connection, opts \\ []) do 33 | optional_params = %{ 34 | :code => :query, 35 | :body => :body 36 | } 37 | 38 | request = 39 | %{} 40 | |> method(:post) 41 | |> url("/api/v1/organization/users") 42 | |> add_optional_params(optional_params, opts) 43 | |> ensure_body() 44 | |> Enum.into([]) 45 | 46 | connection 47 | |> Connection.request(request) 48 | |> evaluate_response([ 49 | {200, %KindeSDK.Model.AddOrganizationUsers200Response{}}, 50 | {204, false}, 51 | {400, false}, 52 | {403, false}, 53 | {404, false} 54 | ]) 55 | end 56 | 57 | @doc """ 58 | Create Organization 59 | Create an organization. 60 | 61 | ### Parameters 62 | 63 | - `connection` (KindeSDK.Connection): Connection to server 64 | - `opts` (keyword): Optional parameters 65 | - `:body` (CreateOrganizationRequest): Organization details. 66 | 67 | ### Returns 68 | 69 | - `{:ok, nil}` on success 70 | - `{:error, Tesla.Env.t}` on failure 71 | """ 72 | @spec create_organization(Tesla.Env.client(), keyword()) :: {:ok, nil} | {:error, Tesla.Env.t()} 73 | def create_organization(connection, opts \\ []) do 74 | optional_params = %{ 75 | :body => :body 76 | } 77 | 78 | request = 79 | %{} 80 | |> method(:post) 81 | |> url("/api/v1/organization") 82 | |> add_optional_params(optional_params, opts) 83 | |> ensure_body() 84 | |> Enum.into([]) 85 | 86 | connection 87 | |> Connection.request(request) 88 | |> evaluate_response([ 89 | {201, false}, 90 | {403, false}, 91 | {404, false}, 92 | {500, false} 93 | ]) 94 | end 95 | 96 | @doc """ 97 | List Organizations 98 | Get a list of organizations. 99 | 100 | ### Parameters 101 | 102 | - `connection` (KindeSDK.Connection): Connection to server 103 | - `opts` (keyword): Optional parameters 104 | - `:sort` (String.t): Field and order to sort the result by. 105 | - `:page_size` (integer()): Number of results per page. Defaults to 10 if parameter not sent. 106 | - `:next_token` (String.t): A string to get the next page of results if there are more results. 107 | 108 | ### Returns 109 | 110 | - `{:ok, [%Organization{}, ...]}` on success 111 | - `{:error, Tesla.Env.t}` on failure 112 | """ 113 | @spec get_orgainzations(Tesla.Env.client(), keyword()) :: 114 | {:ok, list(KindeSDK.Model.Organization.t())} | {:ok, nil} | {:error, Tesla.Env.t()} 115 | def get_orgainzations(connection, opts \\ []) do 116 | optional_params = %{ 117 | :sort => :query, 118 | :page_size => :query, 119 | :next_token => :query 120 | } 121 | 122 | request = 123 | %{} 124 | |> method(:get) 125 | |> url("/api/v1/organizations") 126 | |> add_optional_params(optional_params, opts) 127 | |> Enum.into([]) 128 | 129 | connection 130 | |> Connection.request(request) 131 | |> evaluate_response([ 132 | {200, [%KindeSDK.Model.Organization{}]}, 133 | {403, false} 134 | ]) 135 | end 136 | 137 | @doc """ 138 | Get Organization 139 | Gets an organization given the organization's code. 140 | 141 | ### Parameters 142 | 143 | - `connection` (KindeSDK.Connection): Connection to server 144 | - `opts` (keyword): Optional parameters 145 | - `:code` (String.t): The organization's code. 146 | 147 | ### Returns 148 | 149 | - `{:ok, KindeSDK.Model.Organization.t}` on success 150 | - `{:error, Tesla.Env.t}` on failure 151 | """ 152 | @spec get_organization(Tesla.Env.client(), keyword()) :: 153 | {:ok, nil} | {:ok, KindeSDK.Model.Organization.t()} | {:error, Tesla.Env.t()} 154 | def get_organization(connection, opts \\ []) do 155 | optional_params = %{ 156 | :code => :query 157 | } 158 | 159 | request = 160 | %{} 161 | |> method(:get) 162 | |> url("/api/v1/organization") 163 | |> add_optional_params(optional_params, opts) 164 | |> Enum.into([]) 165 | 166 | connection 167 | |> Connection.request(request) 168 | |> evaluate_response([ 169 | {200, %KindeSDK.Model.Organization{}}, 170 | {400, false}, 171 | {403, false}, 172 | {404, false} 173 | ]) 174 | end 175 | 176 | @doc """ 177 | List Organization Users 178 | Get users in an organizaiton. 179 | 180 | ### Parameters 181 | 182 | - `connection` (KindeSDK.Connection): Connection to server 183 | - `opts` (keyword): Optional parameters 184 | - `:sort` (String.t): Field and order to sort the result by. 185 | - `:page_size` (integer()): Number of results per page. Defaults to 10 if parameter not sent. 186 | - `:next_token` (String.t): A string to get the next page of results if there are more results. 187 | - `:code` (String.t): The organization's code. 188 | 189 | ### Returns 190 | 191 | - `{:ok, KindeSDK.Model.OrganizationUser.t}` on success 192 | - `{:error, Tesla.Env.t}` on failure 193 | """ 194 | @spec get_organization_users(Tesla.Env.client(), keyword()) :: 195 | {:ok, nil} | {:ok, KindeSDK.Model.OrganizationUser.t()} | {:error, Tesla.Env.t()} 196 | def get_organization_users(connection, opts \\ []) do 197 | optional_params = %{ 198 | :sort => :query, 199 | :page_size => :query, 200 | :next_token => :query, 201 | :code => :query 202 | } 203 | 204 | request = 205 | %{} 206 | |> method(:get) 207 | |> url("/api/v1/organization/users") 208 | |> add_optional_params(optional_params, opts) 209 | |> Enum.into([]) 210 | 211 | connection 212 | |> Connection.request(request) 213 | |> evaluate_response([ 214 | {200, %KindeSDK.Model.OrganizationUser{}}, 215 | {403, false} 216 | ]) 217 | end 218 | 219 | @doc """ 220 | Remove Users from an Organization 221 | Remove existing users from an organization. 222 | 223 | ### Parameters 224 | 225 | - `connection` (KindeSDK.Connection): Connection to server 226 | - `opts` (keyword): Optional parameters 227 | - `:code` (String.t): The organization's code. 228 | - `:body` (RemoveOrganizationUsersRequest): 229 | 230 | ### Returns 231 | 232 | - `{:ok, KindeSDK.Model.RemoveOrganizationUsers200Response.t}` on success 233 | - `{:error, Tesla.Env.t}` on failure 234 | """ 235 | @spec remove_organization_users(Tesla.Env.client(), keyword()) :: 236 | {:ok, nil} 237 | | {:ok, KindeSDK.Model.RemoveOrganizationUsers200Response.t()} 238 | | {:error, Tesla.Env.t()} 239 | def remove_organization_users(connection, opts \\ []) do 240 | optional_params = %{ 241 | :code => :query, 242 | :body => :body 243 | } 244 | 245 | request = 246 | %{} 247 | |> method(:patch) 248 | |> url("/api/v1/organization/users") 249 | |> add_optional_params(optional_params, opts) 250 | |> ensure_body() 251 | |> Enum.into([]) 252 | 253 | connection 254 | |> Connection.request(request) 255 | |> evaluate_response([ 256 | {200, %KindeSDK.Model.RemoveOrganizationUsers200Response{}}, 257 | {204, false}, 258 | {400, false}, 259 | {403, false}, 260 | {404, false} 261 | ]) 262 | end 263 | end 264 | -------------------------------------------------------------------------------- /lib/kinde_sdk/api/users.ex: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator 6.3.0 (https://openapi-generator.tech). 2 | # Do not edit this file manually. 3 | 4 | defmodule KindeSDK.Api.Users do 5 | @moduledoc """ 6 | API calls for all endpoints tagged `Users`. 7 | """ 8 | 9 | alias KindeSDK.Connection 10 | import KindeSDK.RequestBuilder 11 | 12 | @doc """ 13 | Assign Users to an Organization 14 | Add existing users to an organization. 15 | 16 | ### Parameters 17 | 18 | - `connection` (KindeSDK.Connection): Connection to server 19 | - `opts` (keyword): Optional parameters 20 | - `:code` (String.t): The organization's code. 21 | - `:body` (AddOrganizationUsersRequest): 22 | 23 | ### Returns 24 | 25 | - `{:ok, KindeSDK.Model.AddOrganizationUsers200Response.t}` on success 26 | - `{:error, Tesla.Env.t}` on failure 27 | """ 28 | @spec add_organization_users(Tesla.Env.client(), keyword()) :: 29 | {:ok, nil} 30 | | {:ok, KindeSDK.Model.AddOrganizationUsers200Response.t()} 31 | | {:error, Tesla.Env.t()} 32 | def add_organization_users(connection, opts \\ []) do 33 | optional_params = %{ 34 | :code => :query, 35 | :body => :body 36 | } 37 | 38 | request = 39 | %{} 40 | |> method(:post) 41 | |> url("/api/v1/organization/users") 42 | |> add_optional_params(optional_params, opts) 43 | |> ensure_body() 44 | |> Enum.into([]) 45 | 46 | connection 47 | |> Connection.request(request) 48 | |> evaluate_response([ 49 | {200, %KindeSDK.Model.AddOrganizationUsers200Response{}}, 50 | {204, false}, 51 | {400, false}, 52 | {403, false}, 53 | {404, false} 54 | ]) 55 | end 56 | 57 | @doc """ 58 | Create User 59 | Creates a user record and optionally zero or more identities for the user. An example identity could be the email address of the user. 60 | 61 | ### Parameters 62 | 63 | - `connection` (KindeSDK.Connection): Connection to server 64 | - `opts` (keyword): Optional parameters 65 | - `:body` (CreateUserRequest): The details of the user to create. 66 | 67 | ### Returns 68 | 69 | - `{:ok, KindeSDK.Model.CreateUser200Response.t}` on success 70 | - `{:error, Tesla.Env.t}` on failure 71 | """ 72 | @spec create_user(Tesla.Env.client(), keyword()) :: 73 | {:ok, nil} | {:ok, KindeSDK.Model.CreateUser200Response.t()} | {:error, Tesla.Env.t()} 74 | def create_user(connection, opts \\ []) do 75 | optional_params = %{ 76 | :body => :body 77 | } 78 | 79 | request = 80 | %{} 81 | |> method(:post) 82 | |> url("/api/v1/user") 83 | |> add_optional_params(optional_params, opts) 84 | |> ensure_body() 85 | |> Enum.into([]) 86 | 87 | connection 88 | |> Connection.request(request) 89 | |> evaluate_response([ 90 | {200, %KindeSDK.Model.CreateUser200Response{}}, 91 | {403, false} 92 | ]) 93 | end 94 | 95 | @doc """ 96 | Delete User 97 | Delete a user record 98 | 99 | ### Parameters 100 | 101 | - `connection` (KindeSDK.Connection): Connection to server 102 | - `opts` (keyword): Optional parameters 103 | - `:id` (String.t): The user's id. 104 | 105 | ### Returns 106 | 107 | - `{:ok, nil}` on success 108 | - `{:error, Tesla.Env.t}` on failure 109 | """ 110 | @spec deleteuser(Tesla.Env.client(), keyword()) :: {:ok, nil} | {:error, Tesla.Env.t()} 111 | def deleteuser(connection, opts \\ []) do 112 | optional_params = %{ 113 | :id => :query 114 | } 115 | 116 | request = 117 | %{} 118 | |> method(:delete) 119 | |> url("/api/v1/user") 120 | |> add_optional_params(optional_params, opts) 121 | |> Enum.into([]) 122 | 123 | connection 124 | |> Connection.request(request) 125 | |> evaluate_response([ 126 | {204, false}, 127 | {400, false}, 128 | {403, false}, 129 | {404, false} 130 | ]) 131 | end 132 | 133 | @doc """ 134 | List Organization Users 135 | Get users in an organizaiton. 136 | 137 | ### Parameters 138 | 139 | - `connection` (KindeSDK.Connection): Connection to server 140 | - `opts` (keyword): Optional parameters 141 | - `:sort` (String.t): Field and order to sort the result by. 142 | - `:page_size` (integer()): Number of results per page. Defaults to 10 if parameter not sent. 143 | - `:next_token` (String.t): A string to get the next page of results if there are more results. 144 | - `:code` (String.t): The organization's code. 145 | 146 | ### Returns 147 | 148 | - `{:ok, KindeSDK.Model.OrganizationUser.t}` on success 149 | - `{:error, Tesla.Env.t}` on failure 150 | """ 151 | @spec get_organization_users(Tesla.Env.client(), keyword()) :: 152 | {:ok, nil} | {:ok, KindeSDK.Model.OrganizationUser.t()} | {:error, Tesla.Env.t()} 153 | def get_organization_users(connection, opts \\ []) do 154 | optional_params = %{ 155 | :sort => :query, 156 | :page_size => :query, 157 | :next_token => :query, 158 | :code => :query 159 | } 160 | 161 | request = 162 | %{} 163 | |> method(:get) 164 | |> url("/api/v1/organization/users") 165 | |> add_optional_params(optional_params, opts) 166 | |> Enum.into([]) 167 | 168 | connection 169 | |> Connection.request(request) 170 | |> evaluate_response([ 171 | {200, %KindeSDK.Model.OrganizationUser{}}, 172 | {403, false} 173 | ]) 174 | end 175 | 176 | @doc """ 177 | Get User 178 | Retrieve a user record 179 | 180 | ### Parameters 181 | 182 | - `connection` (KindeSDK.Connection): Connection to server 183 | - `opts` (keyword): Optional parameters 184 | - `:id` (String.t): The user's id. 185 | 186 | ### Returns 187 | 188 | - `{:ok, KindeSDK.Model.User.t}` on success 189 | - `{:error, Tesla.Env.t}` on failure 190 | """ 191 | @spec get_user(Tesla.Env.client(), keyword()) :: 192 | {:ok, nil} | {:ok, KindeSDK.Model.User.t()} | {:error, Tesla.Env.t()} 193 | def get_user(connection, opts \\ []) do 194 | optional_params = %{ 195 | :id => :query 196 | } 197 | 198 | request = 199 | %{} 200 | |> method(:get) 201 | |> url("/api/v1/user") 202 | |> add_optional_params(optional_params, opts) 203 | |> Enum.into([]) 204 | 205 | connection 206 | |> Connection.request(request) 207 | |> evaluate_response([ 208 | {200, %KindeSDK.Model.User{}}, 209 | {400, false}, 210 | {403, false}, 211 | {404, false} 212 | ]) 213 | end 214 | 215 | @doc """ 216 | List Users 217 | The returned list can be sorted by full name or email address in ascending or descending order. The number of records to return at a time can also be controlled using the `page_size` query string parameter. 218 | 219 | ### Parameters 220 | 221 | - `connection` (KindeSDK.Connection): Connection to server 222 | - `opts` (keyword): Optional parameters 223 | - `:sort` (String.t): Field and order to sort the result by. 224 | - `:page_size` (integer()): Number of results per page. Defaults to 10 if parameter not sent. 225 | - `:user_id` (integer()): ID of the user to filter by. 226 | - `:next_token` (String.t): A string to get the next page of results if there are more results. 227 | 228 | ### Returns 229 | 230 | - `{:ok, [%User{}, ...]}` on success 231 | - `{:error, Tesla.Env.t}` on failure 232 | """ 233 | @spec get_users(Tesla.Env.client(), keyword()) :: 234 | {:ok, nil} | {:ok, list(KindeSDK.Model.User.t())} | {:error, Tesla.Env.t()} 235 | def get_users(connection, opts \\ []) do 236 | optional_params = %{ 237 | :sort => :query, 238 | :page_size => :query, 239 | :user_id => :query, 240 | :next_token => :query 241 | } 242 | 243 | request = 244 | %{} 245 | |> method(:get) 246 | |> url("/api/v1/users") 247 | |> add_optional_params(optional_params, opts) 248 | |> Enum.into([]) 249 | 250 | connection 251 | |> Connection.request(request) 252 | |> evaluate_response([ 253 | {200, [%KindeSDK.Model.User{}]}, 254 | {403, false} 255 | ]) 256 | end 257 | 258 | @doc """ 259 | Remove Users from an Organization 260 | Remove existing users from an organization. 261 | 262 | ### Parameters 263 | 264 | - `connection` (KindeSDK.Connection): Connection to server 265 | - `opts` (keyword): Optional parameters 266 | - `:code` (String.t): The organization's code. 267 | - `:body` (RemoveOrganizationUsersRequest): 268 | 269 | ### Returns 270 | 271 | - `{:ok, KindeSDK.Model.RemoveOrganizationUsers200Response.t}` on success 272 | - `{:error, Tesla.Env.t}` on failure 273 | """ 274 | @spec remove_organization_users(Tesla.Env.client(), keyword()) :: 275 | {:ok, nil} 276 | | {:ok, KindeSDK.Model.RemoveOrganizationUsers200Response.t()} 277 | | {:error, Tesla.Env.t()} 278 | def remove_organization_users(connection, opts \\ []) do 279 | optional_params = %{ 280 | :code => :query, 281 | :body => :body 282 | } 283 | 284 | request = 285 | %{} 286 | |> method(:patch) 287 | |> url("/api/v1/organization/users") 288 | |> add_optional_params(optional_params, opts) 289 | |> ensure_body() 290 | |> Enum.into([]) 291 | 292 | connection 293 | |> Connection.request(request) 294 | |> evaluate_response([ 295 | {200, %KindeSDK.Model.RemoveOrganizationUsers200Response{}}, 296 | {204, false}, 297 | {400, false}, 298 | {403, false}, 299 | {404, false} 300 | ]) 301 | end 302 | 303 | @doc """ 304 | Update User 305 | Update a user record 306 | 307 | ### Parameters 308 | 309 | - `connection` (KindeSDK.Connection): Connection to server 310 | - `update_user_request` (UpdateUserRequest): The user to update. 311 | - `opts` (keyword): Optional parameters 312 | - `:id` (String.t): The user's id. 313 | 314 | ### Returns 315 | 316 | - `{:ok, KindeSDK.Model.User.t}` on success 317 | - `{:error, Tesla.Env.t}` on failure 318 | """ 319 | @spec update_user(Tesla.Env.client(), KindeSDK.Model.UpdateUserRequest.t(), keyword()) :: 320 | {:ok, nil} | {:ok, KindeSDK.Model.User.t()} | {:error, Tesla.Env.t()} 321 | def update_user(connection, update_user_request, opts \\ []) do 322 | optional_params = %{ 323 | :id => :query 324 | } 325 | 326 | request = 327 | %{} 328 | |> method(:patch) 329 | |> url("/api/v1/user") 330 | |> add_param(:body, :body, update_user_request) 331 | |> add_optional_params(optional_params, opts) 332 | |> Enum.into([]) 333 | 334 | connection 335 | |> Connection.request(request) 336 | |> evaluate_response([ 337 | {200, %KindeSDK.Model.User{}}, 338 | {400, false}, 339 | {403, false}, 340 | {404, false} 341 | ]) 342 | end 343 | end 344 | -------------------------------------------------------------------------------- /lib/kinde_sdk/connection.ex: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator 6.3.0 (https://openapi-generator.tech). 2 | # Do not edit this file manually. 3 | 4 | defmodule KindeSDK.Connection do 5 | @moduledoc """ 6 | Handle Tesla connections for KindeSDK. 7 | 8 | Additional middleware can be set in the compile-time or runtime configuration: 9 | 10 | config :tesla, KindeSDK.Connection, 11 | base_url: "https://app.kinde.com", 12 | adapter: Tesla.Adapter.Hackney 13 | 14 | The default base URL can also be set as: 15 | 16 | config :kinde_sdk, 17 | :base_url, "https://app.kinde.com" 18 | """ 19 | 20 | @default_base_url Application.compile_env( 21 | :kinde_sdk, 22 | :base_url, 23 | "https://app.kinde.com" 24 | ) 25 | 26 | @typedoc """ 27 | The list of options that can be passed to new/1. 28 | 29 | - `base_url`: Overrides the base URL on a per-client basis. 30 | - `user_agent`: Overrides the User-Agent header. 31 | """ 32 | @type options :: [ 33 | {:base_url, String.t()}, 34 | {:user_agent, String.t()} 35 | ] 36 | 37 | @doc "Forward requests to Tesla." 38 | @spec request(Tesla.Client.t(), [Tesla.option()]) :: Tesla.Env.result() 39 | defdelegate request(client, options), to: Tesla 40 | 41 | @doc """ 42 | Configure a client with no authentication. 43 | 44 | ### Returns 45 | 46 | Tesla.Env.client 47 | """ 48 | @spec new() :: Tesla.Env.client() 49 | def new do 50 | Tesla.client(middleware(), adapter()) 51 | end 52 | 53 | @doc """ 54 | Configure a client that may have authentication. 55 | 56 | ### Parameters 57 | 58 | - `options`: a keyword list of OpenAPIPetstore.Connection.options. 59 | 60 | ### Returns 61 | 62 | Tesla.Env.client 63 | """ 64 | @spec new(options) :: Tesla.Env.client() 65 | 66 | def new(options) when is_list(options) do 67 | options 68 | |> middleware() 69 | |> Tesla.client(adapter()) 70 | end 71 | 72 | @doc """ 73 | Returns fully configured middleware for passing to Tesla.client/2. 74 | """ 75 | @spec middleware(options) :: [Tesla.Client.middleware()] 76 | def middleware(options \\ []) do 77 | base_url = 78 | Keyword.get( 79 | options, 80 | :base_url, 81 | Application.get_env(:kinde_sdk, :base_url, @default_base_url) 82 | ) 83 | 84 | tesla_options = Application.get_env(:tesla, __MODULE__, []) 85 | middleware = Keyword.get(tesla_options, :middleware, []) 86 | json_engine = Keyword.get(tesla_options, :json, Poison) 87 | 88 | user_agent = 89 | Keyword.get( 90 | options, 91 | :user_agent, 92 | Keyword.get( 93 | tesla_options, 94 | :user_agent, 95 | "openapi-generator - KindeSDK 1 - elixir" 96 | ) 97 | ) 98 | 99 | [ 100 | {Tesla.Middleware.BaseUrl, base_url}, 101 | {Tesla.Middleware.Headers, [{"user-agent", user_agent}]}, 102 | {Tesla.Middleware.EncodeJson, engine: json_engine} 103 | | middleware 104 | ] 105 | end 106 | 107 | @doc """ 108 | Returns the default adapter for this API. 109 | """ 110 | def adapter do 111 | :tesla 112 | |> Application.get_env(__MODULE__, []) 113 | |> Keyword.get(:adapter, nil) 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/kinde_sdk/deserializer.ex: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator 6.3.0 (https://openapi-generator.tech). 2 | # Do not edit this file manually. 3 | 4 | defmodule KindeSDK.Deserializer do 5 | @moduledoc """ 6 | Helper functions for deserializing responses into models 7 | """ 8 | 9 | @doc """ 10 | Update the provided model with a deserialization of a nested value 11 | """ 12 | @spec deserialize(struct(), :atom, :atom, struct(), keyword()) :: struct() 13 | def deserialize(model, field, :list, mod, options) do 14 | model 15 | |> Map.update!(field, &Poison.Decode.decode(&1, Keyword.merge(options, as: [struct(mod)]))) 16 | end 17 | 18 | def deserialize(model, field, :struct, mod, options) do 19 | model 20 | |> Map.update!(field, &Poison.Decode.decode(&1, Keyword.merge(options, as: struct(mod)))) 21 | end 22 | 23 | def deserialize(model, field, :map, mod, options) do 24 | maybe_transform_map = fn 25 | nil -> 26 | nil 27 | 28 | existing_value -> 29 | Map.new(existing_value, fn 30 | {key, val} -> 31 | {key, Poison.Decode.decode(val, Keyword.merge(options, as: struct(mod)))} 32 | end) 33 | end 34 | 35 | Map.update!(model, field, maybe_transform_map) 36 | end 37 | 38 | def deserialize(model, field, :date, _, _options) do 39 | value = Map.get(model, field) 40 | 41 | case is_binary(value) do 42 | true -> 43 | case DateTime.from_iso8601(value) do 44 | {:ok, datetime, _offset} -> Map.put(model, field, datetime) 45 | _ -> model 46 | end 47 | 48 | false -> 49 | model 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/kinde_sdk/kinde_cache.ex: -------------------------------------------------------------------------------- 1 | defmodule KindeSDK.KindeCache do 2 | @moduledoc """ 3 | Kinde Cache GenServer 4 | 5 | This module defines a GenServer for caching data in the Kinde SDK. 6 | It provides methods to store and retrieve data from an ETS table for efficient data access. 7 | 8 | ## Usage Example 9 | 10 | To use the Kinde Cache GenServer, you can start it and then interact with it as follows: 11 | 12 | ```elixir 13 | {:ok, pid} = KindeSDK.KindeCache.start_link() 14 | KindeSDK.KindeCache.add_kinde_data(pid, {:some_key, "cached_data"}) 15 | data = KindeSDK.KindeCache.get_kinde_data(pid, :some_key) 16 | 17 | This GenServer is designed to help improve the performance of data storage 18 | and retrieval in Kinde applications. 19 | """ 20 | use GenServer 21 | 22 | def start_link() do 23 | GenServer.start_link(__MODULE__, [], []) 24 | end 25 | 26 | def init(_) do 27 | table_id = :ets.new(:kinde_cache, [:set, :public]) 28 | {:ok, %{table_id: table_id, state: []}} 29 | end 30 | 31 | def get_data(pid) do 32 | {:ok, state} = GenServer.call(pid, :get_data) 33 | state 34 | end 35 | 36 | def handle_call({:get_kinde_data, key}, _from, %{table_id: table_id, state: _state} = state) do 37 | data = :ets.lookup(table_id, key) 38 | {:reply, data, state} 39 | end 40 | 41 | def handle_cast({:add_kinde_data, data}, %{table_id: table_id, state: _state} = state) do 42 | :ets.insert(table_id, data) 43 | {:noreply, state} 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/kinde_sdk/kinde_client_sdk.ex: -------------------------------------------------------------------------------- 1 | defmodule KindeClientSDK do 2 | @moduledoc """ 3 | Kinde Client supporting `client_credentials`, `authorization_code` and `pkce` OAuth2 methods, 4 | providing authentication functionalities to your Kinde business. 5 | 6 | ## Configuration 7 | 8 | ### API Keys 9 | 10 | You can set your keys in your application configuration. Use `config/config.exs`. 11 | For example: 12 | 13 | config :kinde_sdk, 14 | backend_client_id: "test_x1y2z3a1", 15 | frontend_client_id: "test_a1b2c3d4", 16 | client_secret: "test_112233", 17 | redirect_url: "http://text.com/callback", 18 | domain: "https://test.kinde.com", 19 | logout_redirect_url: "http://text.com/logout" 20 | 21 | Optionally, you can also set `scope` as well. 22 | config :kinde_sdk, 23 | scope: "email" 24 | 25 | You can also use `System.get_env/1` to retrieve the API key from 26 | an environment variables. For example: 27 | 28 | config :kinde_sdk, 29 | backend_client_id: System.get_env("KINDE_BACKEND_CLIENT_ID") 30 | 31 | ## Usage 32 | 33 | Initialize your client like this: 34 | 35 | {conn, client} = 36 | KindeClientSDK.init( 37 | conn, 38 | Application.get_env(:kinde_sdk, :domain), 39 | Application.get_env(:kinde_sdk, :redirect_url), 40 | Application.get_env(:kinde_sdk, :backend_client_id), 41 | Application.get_env(:kinde_sdk, :client_secret), 42 | :client_credentials, 43 | Application.get_env(:kinde_sdk, :logout_redirect_url) 44 | ) 45 | 46 | `conn` will be phoenix connection here here. Now, you can use other implemented client functions, 47 | like `login` and `get_token`. 48 | 49 | ### ETS Cache 50 | 51 | KindeClientSDK implements persistant ETS cache for storing the client data and authenticating 52 | variables. 53 | 54 | You may call your created client like this: 55 | 56 | client = KindeClientSDK.get_kinde_client(conn) 57 | 58 | ### Tokens 59 | 60 | We can use Kinde cache to get the tokens generated by `login` and `get_token` functions. 61 | 62 | KindeClientSDK.get_all_data(conn) 63 | """ 64 | alias KindeSDK.SDK.AuthorizationCode 65 | alias KindeSDK.SDK.ClientCredentials 66 | alias KindeSDK.SDK.Utils 67 | alias KindeSDK.SDK.Pkce 68 | alias KindeSDK.KindeCache 69 | alias Plug.Conn 70 | use Tesla 71 | 72 | @authorization_end_point "/oauth2/auth" 73 | @token_end_point "/oauth2/token" 74 | @logout_end_point "/logout" 75 | 76 | @enforce_keys [ 77 | :cache_pid, 78 | :domain, 79 | :redirect_uri, 80 | :client_secret, 81 | :client_id, 82 | :grant_type, 83 | :logout_endpoint 84 | ] 85 | defstruct [ 86 | :cache_pid, 87 | :domain, 88 | :redirect_uri, 89 | :logout_redirect_uri, 90 | :client_id, 91 | :client_secret, 92 | :authorization_endpoint, 93 | :token_endpoint, 94 | :logout_endpoint, 95 | :grant_type, 96 | :auth_status, 97 | :additional_params, 98 | :scopes 99 | ] 100 | 101 | @type t :: %KindeClientSDK{ 102 | additional_params: map, 103 | auth_status: atom, 104 | authorization_endpoint: binary, 105 | cache_pid: pid, 106 | client_id: any, 107 | client_secret: any, 108 | domain: binary, 109 | grant_type: any, 110 | logout_endpoint: binary, 111 | logout_redirect_uri: any, 112 | redirect_uri: binary, 113 | scopes: any, 114 | token_endpoint: binary 115 | } 116 | 117 | @doc """ 118 | Used for initializing the Kinde client which will be then used other implemented client functions, 119 | like `login` and `get_token`. 120 | 121 | Initialize your client like this: 122 | 123 | {conn, client} = 124 | KindeClientSDK.init( 125 | conn, 126 | Application.get_env(:kinde_sdk, :domain), 127 | Application.get_env(:kinde_sdk, :redirect_url), 128 | Application.get_env(:kinde_sdk, :backend_client_id), 129 | Application.get_env(:kinde_sdk, :client_secret), 130 | :client_credentials, 131 | Application.get_env(:kinde_sdk, :logout_redirect_url) 132 | ) 133 | 134 | `conn` will be phoenix connection here here. 135 | 136 | `scopes` is an optional and defaults to `"openid profile email offline"`. Scopes can be defined as string. 137 | 138 | `additional_params` is an optional and defaults to `%{}`. It accepts `:audience`, `:org_code` and 139 | `:org_name` keys in the map with string as values. 140 | 141 | Throws for invalid `domain`, `redirect_uri` and `additional_params`. 142 | """ 143 | @spec init(Plug.Conn.t(), binary, binary, any, any, any, any, binary, map) :: 144 | {Plug.Conn.t(), map()} 145 | def init( 146 | conn, 147 | domain, 148 | redirect_uri, 149 | client_id, 150 | client_secret, 151 | grant_type, 152 | logout_redirect_uri, 153 | scopes \\ "openid profile email offline", 154 | additional_params \\ %{} 155 | ) do 156 | if !Utils.validate_url(domain) do 157 | throw("Please provide valid domain") 158 | end 159 | 160 | if !Utils.validate_url(redirect_uri) do 161 | throw("Please provide valid redirect_uri") 162 | end 163 | 164 | {:ok, pid} = KindeCache.start_link() 165 | conn = Conn.put_session(conn, :kinde_cache_pid, pid) 166 | 167 | client = %__MODULE__{ 168 | cache_pid: pid, 169 | domain: domain, 170 | redirect_uri: redirect_uri, 171 | client_secret: client_secret, 172 | client_id: client_id, 173 | grant_type: grant_type, 174 | additional_params: Utils.check_additional_params(additional_params), 175 | logout_redirect_uri: logout_redirect_uri, 176 | scopes: scopes, 177 | authorization_endpoint: "#{domain}#{@authorization_end_point}", 178 | token_endpoint: "#{domain}#{@token_end_point}", 179 | logout_endpoint: "#{domain}#{@logout_end_point}", 180 | auth_status: :unauthenticated 181 | } 182 | 183 | save_kinde_client(conn, client) 184 | 185 | {conn, client} 186 | end 187 | 188 | @doc """ 189 | Login function for KindeClient. If grant type is `:client_credentials`, then access token will be generated 190 | and stored to KindeCache. For other grant types, returned `conn` will redirect to the your Kinde business login page. 191 | 192 | Your callback function will call `get_token` function to get the authenticated user's token. 193 | 194 | ### Usage 195 | 196 | conn = KindeClientSDK.login(conn, client) 197 | 198 | `additional_params` is an optional and defaults to `%{}`. It accepts `:audience`, `:org_code` and 199 | `:org_name` keys in the map with string as values. 200 | 201 | Throws for invalid `grant_type`. 202 | """ 203 | @spec login( 204 | Plug.Conn.t(), 205 | KindeClientSDK.t(), 206 | map() 207 | ) :: any 208 | def login(conn, client, additional_params \\ %{}) do 209 | clean_session(client.cache_pid) 210 | client = update_auth_status(client, :authenticating) 211 | 212 | case client.grant_type do 213 | :client_credentials -> 214 | ClientCredentials.login(conn, client, additional_params) 215 | 216 | :authorization_code -> 217 | AuthorizationCode.login(conn, client, additional_params) 218 | 219 | :authorization_code_flow_pkce -> 220 | Pkce.login(conn, client, :login, additional_params) 221 | 222 | _ -> 223 | update_auth_status(client, :unauthenticated) 224 | throw("Please provide correct grant_type") 225 | end 226 | end 227 | 228 | @doc """ 229 | Register function for your KindeClient. Returing `conn` will redirect to the your Kinde business registration page. 230 | 231 | ### Usage 232 | 233 | conn = KindeClientSDK.register(conn, client) 234 | 235 | `additional_params` is an optional and defaults to `%{}`. It accepts `:audience`, `:org_code` and 236 | `:org_name` keys in the map with string as values. 237 | """ 238 | @spec register( 239 | Plug.Conn.t(), 240 | KindeClientSDK.t(), 241 | map() 242 | ) :: Plug.Conn.t() 243 | def register(conn, client, additional_params \\ %{}) do 244 | client = update_auth_status(client, :authenticating) 245 | client = %{client | grant_type: :authorization_code} 246 | Pkce.login(conn, client, :registration, additional_params) 247 | end 248 | 249 | @doc """ 250 | The function allows you to create organization. Similar to register, the returing `conn` will redirect 251 | to the your Kinde business registration page. 252 | 253 | ### Usage 254 | 255 | conn = KindeClientSDK.create_org(conn, client) 256 | 257 | `additional_params` is an optional and defaults to `%{}`. It accepts `:audience`, `:org_code` and 258 | `:org_name` keys in the map with string as values. 259 | """ 260 | @spec create_org( 261 | Plug.Conn.t(), 262 | KindeClientSDK.t(), 263 | map() 264 | ) :: Plug.Conn.t() 265 | def create_org(conn, client, additional_params \\ %{}) do 266 | additional_params = Map.put(additional_params, :is_create_org, "true") 267 | register(conn, client, additional_params) 268 | end 269 | 270 | @doc """ 271 | Fetches the tokens for your KindeClient. The tokens can be obtained through Kinde cache afterwards. 272 | 273 | ### Usage 274 | 275 | {conn, client} = KindeClientSDK.get_token(conn) 276 | 277 | Get the tokens like this: 278 | 279 | pid = Conn.get_session(conn, :kinde_cache_pid) 280 | GenServer.call(pid, {:get_kinde_data, :kinde_access_token}) 281 | """ 282 | @spec get_token(Plug.Conn.t()) :: 283 | {Plug.Conn.t(), map()} 284 | def get_token(conn) do 285 | client = get_kinde_client(conn) 286 | do_get_token(conn, client.grant_type) 287 | end 288 | 289 | defp do_get_token(conn, :client_credentials) do 290 | client = get_kinde_client(conn) 291 | 292 | conn = ClientCredentials.login(conn, client) 293 | 294 | client = get_kinde_client(conn) 295 | {conn, client} 296 | end 297 | 298 | defp do_get_token(conn, grant_type) 299 | when grant_type in [:authorization_code, :authorization_code_flow_pkce] do 300 | client = get_kinde_client(conn) 301 | expiring_timestamp = return_key(client.cache_pid, :kinde_expiring_time_stamp) 302 | 303 | form_params = %{ 304 | client_id: client.client_id, 305 | client_secret: client.client_secret, 306 | grant_type: get_grant_type(client.grant_type), 307 | redirect_uri: client.redirect_uri, 308 | response_type: :code 309 | } 310 | 311 | if is_nil(expiring_timestamp) do 312 | %{"state" => state, "code" => authorization_code} = 313 | Conn.fetch_query_params(conn).query_params 314 | 315 | check_state_authentication(client.cache_pid, state) 316 | 317 | error = Conn.fetch_query_params(conn).query_params["error"] 318 | 319 | if error do 320 | error_description = Conn.fetch_query_params(conn).query_params["error_description"] 321 | message = error_description || error 322 | throw(message) 323 | end 324 | 325 | if is_nil(authorization_code), do: throw("Not found code param") 326 | form_params = Map.put(form_params, :code, authorization_code) 327 | 328 | code_verifier = get_all_data(conn).oauth_code_verifier 329 | 330 | form_params = 331 | cond do 332 | code_verifier -> 333 | Map.put(form_params, :code_verifier, code_verifier) 334 | 335 | client.grant_type == :authorization_code_flow_pkce -> 336 | throw("Not found code_verifier") 337 | 338 | true -> 339 | form_params 340 | end 341 | 342 | get_a_new_token(form_params, client) 343 | else 344 | fetch_token_with_expiring_timestamps(conn, expiring_timestamp, form_params, client) 345 | end 346 | 347 | client = update_auth_status(client, :authenticated) 348 | save_kinde_client(conn, client) 349 | 350 | {conn, client} 351 | end 352 | 353 | defp do_get_token(_, _) do 354 | throw("Please provide correct grant_type") 355 | end 356 | 357 | defp fetch_token_with_expiring_timestamps(conn, expiring_timestamp, form_params, client) do 358 | case DateTime.compare(expiring_timestamp, DateTime.utc_now()) do 359 | :gt -> 360 | get_all_data(conn) 361 | 362 | :lt -> 363 | refresh_token_params = 364 | form_params 365 | |> Map.merge(%{ 366 | grant_type: :refresh_token, 367 | refresh_token: return_key(client.cache_pid, :kinde_refresh_token) 368 | }) 369 | 370 | get_a_new_token(refresh_token_params, client) 371 | 372 | _ -> 373 | "Access/Refresh Tokens are invalid" 374 | end 375 | end 376 | 377 | defp get_a_new_token(params, client) do 378 | body = {:form, params |> Map.to_list()} 379 | 380 | {:ok, response} = 381 | HTTPoison.post( 382 | client.token_endpoint, 383 | body, 384 | [ 385 | {"Kinde-SDK", "Elixir/#{Utils.get_current_app_version()}"} 386 | ] 387 | ) 388 | 389 | body = Jason.decode!(response.body) 390 | GenServer.cast(client.cache_pid, {:add_kinde_data, {:kinde_token, body}}) 391 | save_data_to_session(client.cache_pid, body) 392 | end 393 | 394 | defp save_data_to_session(pid, token) do 395 | expires_in = if is_nil(token["expires_in"]), do: 0, else: token["expires_in"] 396 | 397 | GenServer.cast(pid, {:add_kinde_data, {:kinde_login_time_stamp, DateTime.utc_now()}}) 398 | 399 | GenServer.cast( 400 | pid, 401 | {:add_kinde_data, 402 | {:kinde_expiring_time_stamp, 403 | Utils.calculate_expiring_timestamp(DateTime.utc_now(), expires_in)}} 404 | ) 405 | 406 | GenServer.cast(pid, {:add_kinde_data, {:kinde_access_token, token["access_token"]}}) 407 | GenServer.cast(pid, {:add_kinde_data, {:kinde_id_token, token["id_token"]}}) 408 | GenServer.cast(pid, {:add_kinde_data, {:kinde_refresh_token, token["refresh_token"]}}) 409 | GenServer.cast(pid, {:add_kinde_data, {:kinde_expires_in, expires_in}}) 410 | 411 | payload = Utils.parse_jwt(token["id_token"]) 412 | 413 | if is_nil(payload) do 414 | GenServer.cast(pid, {:add_kinde_data, {:kinde_user, nil}}) 415 | else 416 | user = %{ 417 | id: payload["sub"], 418 | given_name: payload["given_name"], 419 | family_name: payload["family_name"], 420 | email: payload["email"], 421 | picture: payload["picture"] 422 | } 423 | 424 | GenServer.cast( 425 | pid, 426 | {:add_kinde_data, {:kinde_user, user}} 427 | ) 428 | end 429 | end 430 | 431 | @doc """ 432 | Returns the user details after successful authentication. 433 | 434 | ### Usage 435 | KindeClientSDK.get_user_detail(conn) 436 | 437 | Returns `nil` if not authenticated or if grant type is `:client_credentials`. 438 | """ 439 | @spec get_user_detail(Plug.Conn.t()) :: any 440 | def get_user_detail(conn) do 441 | data = get_all_data(conn) 442 | 443 | data.user 444 | end 445 | 446 | @doc """ 447 | Log outs your client. Returned `conn` redirects to you to Kinde logout page and 448 | returns back to your logout redirect url. 449 | 450 | ### Usage 451 | conn = KindeClientSDK.logout(conn) 452 | """ 453 | @spec logout(Plug.Conn.t()) :: Plug.Conn.t() 454 | def logout(conn) do 455 | client = get_kinde_client(conn) 456 | 457 | pid = Conn.get_session(conn, :kinde_cache_pid) 458 | clean_session(pid) 459 | client = update_auth_status(client, :unauthenticated) 460 | save_kinde_client(conn, client) 461 | 462 | search_params = %{redirect: client.logout_redirect_uri} 463 | 464 | query_params = URI.encode_query(search_params) 465 | 466 | conn = 467 | conn 468 | |> Plug.Conn.resp(:found, "") 469 | |> Plug.Conn.put_resp_header("location", "#{client.logout_endpoint}?#{query_params}") 470 | 471 | conn 472 | end 473 | 474 | @doc """ 475 | Returns new grant type. Used by `get_token` function. 476 | """ 477 | @spec get_grant_type(:authorization_code | :authorization_code_flow_pkce | :client_credentials) :: 478 | :authorization_code | :client_credentials 479 | def get_grant_type(type) do 480 | case type do 481 | :client_credentials -> 482 | :client_credentials 483 | 484 | :authorization_code -> 485 | :authorization_code 486 | 487 | :authorization_code_flow_pkce -> 488 | :authorization_code 489 | 490 | _ -> 491 | throw("Please provide correct grant_type") 492 | end 493 | end 494 | 495 | @doc """ 496 | Returns whether if a user is logged in by verifying that the access token is still valid. 497 | 498 | ### Usage 499 | KindeClientSDK.authenticated?(conn) 500 | """ 501 | @spec authenticated?(Plug.Conn.t()) :: boolean 502 | def authenticated?(conn) do 503 | data = get_all_data(conn) 504 | 505 | timestamp = data.login_time_stamp 506 | expires_in = data.expires_in 507 | 508 | if is_nil(timestamp) or is_nil(expires_in) do 509 | false 510 | else 511 | DateTime.diff(DateTime.utc_now(), timestamp) < expires_in 512 | end 513 | end 514 | 515 | @doc """ 516 | Returns token claims and their value. 517 | 518 | ### Usage 519 | 520 | Second argument defaults to `:access_token` 521 | 522 | KindeClientSDK.get_claims(conn) 523 | 524 | If used with `:id_token` 525 | 526 | KindeClientSDK.get_claims(conn, :id_token) 527 | """ 528 | @spec get_claims(Plug.Conn.t(), any) :: any 529 | def get_claims(conn, token_type \\ :access_token) do 530 | if !(token_type in [:access_token, :id_token]) do 531 | throw("Please provide valid token (access_token or id_token) to get claim") 532 | end 533 | 534 | key = String.to_atom("kinde_#{token_type}") 535 | 536 | pid = Conn.get_session(conn, :kinde_cache_pid) 537 | 538 | token = 539 | case GenServer.call(pid, {:get_kinde_data, key}) do 540 | [kinde_access_token: data] -> data 541 | [kinde_id_token: data] -> data 542 | _ -> nil 543 | end 544 | 545 | if is_nil(token) do 546 | throw("Request is missing required authentication credential") 547 | end 548 | 549 | Utils.parse_jwt(token) 550 | end 551 | 552 | @doc """ 553 | Returns a single claim object from token and its name and value. 554 | 555 | ### Usage 556 | 557 | Third argument defaults to `:access_token` 558 | 559 | KindeClientSDK.get_claim(conn, "jti") 560 | 561 | If used with `:id_token` 562 | 563 | KindeClientSDK.get_claim(conn, "jti", :id_token) 564 | """ 565 | @spec get_claim(Plug.Conn.t(), any, any) :: any 566 | def get_claim(conn, key, token_type \\ :access_token) do 567 | data = get_claims(conn, token_type) 568 | 569 | %{ 570 | name: key, 571 | value: data[key] 572 | } 573 | end 574 | 575 | @doc """ 576 | Returns an object with a list of permissions and also the relevant org code. 577 | 578 | ### Usage 579 | 580 | KindeClientSDK.get_permissions(conn) 581 | KindeClientSDK.get_permissions(conn, :id_token) 582 | """ 583 | @spec get_permissions(Plug.Conn.t(), any) :: %{org_code: any, permissions: any} 584 | def get_permissions(conn, token_type \\ :access_token) do 585 | claims = get_claims(conn, token_type) 586 | %{org_code: claims["org_code"], permissions: claims["permissions"]} 587 | end 588 | 589 | @doc """ 590 | Given a permission value, returns if it is granted or not and relevant org code. 591 | 592 | ### Usage 593 | 594 | KindeClientSDK.get_permission(conn, "create:users") 595 | KindeClientSDK.get_permission(conn, "create:users", :id_token) 596 | """ 597 | @spec get_permission(Plug.Conn.t(), any, any) :: %{is_granted: boolean, org_code: any} 598 | def get_permission(conn, permission, token_type \\ :access_token) do 599 | all_claims = get_claims(conn, token_type) 600 | permissions = all_claims["permissions"] 601 | %{org_code: all_claims["org_code"], is_granted: permission in permissions} 602 | end 603 | 604 | @doc """ 605 | Returns the org code from the claims. 606 | 607 | ### Usage 608 | KindeClientSDK.get_user_organization(conn) 609 | """ 610 | @spec get_organization(Plug.Conn.t()) :: %{org_code: any} 611 | def get_organization(conn) do 612 | %{org_code: get_claim(conn, "org_code", :id_token)} 613 | end 614 | 615 | @doc """ 616 | Returns the org code from the user token. 617 | 618 | ### Usage 619 | KindeClientSDK.get_user_organizations(conn) 620 | """ 621 | @spec get_user_organizations(Plug.Conn.t()) :: %{org_codes: any} 622 | def get_user_organizations(conn) do 623 | %{org_codes: get_claim(conn, "org_codes", :id_token)} 624 | end 625 | 626 | @doc """ 627 | Returns the authentication status. 628 | 629 | ### Usage 630 | KindeClientSDK.get_auth_status(conn) 631 | """ 632 | @spec get_auth_status(Plug.Conn.t()) :: any 633 | def get_auth_status(conn) do 634 | Conn.get_session(conn, :kinde_auth_status) 635 | end 636 | 637 | defp update_auth_status(client, status) do 638 | GenServer.cast(client.cache_pid, {:add_kinde_data, {:kinde_auth_status, status}}) 639 | %{client | auth_status: status} 640 | end 641 | 642 | defp clean_session(pid) do 643 | GenServer.cast(pid, {:add_kinde_data, {:kinde_token, nil}}) 644 | GenServer.cast(pid, {:add_kinde_data, {:kinde_access_token, nil}}) 645 | GenServer.cast(pid, {:add_kinde_data, {:kinde_id_token, nil}}) 646 | GenServer.cast(pid, {:add_kinde_data, {:kinde_auth_status, nil}}) 647 | GenServer.cast(pid, {:add_kinde_data, {:kinde_oauth_state, nil}}) 648 | GenServer.cast(pid, {:add_kinde_data, {:kinde_oauth_code_verifier, nil}}) 649 | GenServer.cast(pid, {:add_kinde_data, {:kinde_expires_in, nil}}) 650 | GenServer.cast(pid, {:add_kinde_data, {:kinde_login_time_stamp, nil}}) 651 | GenServer.cast(pid, {:add_kinde_data, {:kinde_user, nil}}) 652 | end 653 | 654 | defp check_state_authentication(pid, server_state) do 655 | [kinde_oauth_state: oauth_state] = GenServer.call(pid, {:get_kinde_data, :kinde_oauth_state}) 656 | 657 | if is_nil(oauth_state) or server_state != oauth_state do 658 | throw("Authentication failed because it tries to validate state") 659 | end 660 | end 661 | 662 | @doc """ 663 | Returns the Kinde client created from the `conn`. 664 | 665 | ### Usage 666 | client = KindeClientSDK.get_kinde_client(conn) 667 | """ 668 | @spec get_kinde_client(Plug.Conn.t()) :: map() 669 | def get_kinde_client(conn) do 670 | [kinde_client: client] = 671 | Conn.get_session(conn, :kinde_cache_pid) 672 | |> GenServer.call({:get_kinde_data, :kinde_client}) 673 | 674 | client 675 | end 676 | 677 | @doc """ 678 | Saves the Kinde client created into the `conn`. 679 | 680 | ### Usage 681 | KindeClientSDK.save_kinde_client(conn) 682 | """ 683 | @spec save_kinde_client(Plug.Conn.t(), any) :: :ok 684 | def save_kinde_client(conn, client) do 685 | Conn.get_session(conn, :kinde_cache_pid) 686 | |> GenServer.cast({:add_kinde_data, {:kinde_client, client}}) 687 | end 688 | 689 | @doc """ 690 | Returns the Kinde cache PID from the `conn`. 691 | 692 | ### Usage 693 | pid = KindeClientSDK.get_cache_pid(conn) 694 | """ 695 | @spec get_cache_pid(Plug.Conn.t()) :: any 696 | def get_cache_pid(conn) do 697 | Conn.get_session(conn, :kinde_cache_pid) 698 | end 699 | 700 | @doc """ 701 | Returns all the Kinde data (tokens) returned. 702 | 703 | ### Usage 704 | data = KindeClientSDK.get_all_data(conn) 705 | IO.inspect(data.access_token, label: "Access Token") 706 | """ 707 | @spec get_all_data(Plug.Conn.t()) :: %{ 708 | access_token: any, 709 | refresh_token: any, 710 | expires_in: any, 711 | id_token: any, 712 | login_time_stamp: any, 713 | expiring_time_stamp: any, 714 | token: any, 715 | user: any, 716 | oauth_code_verifier: any 717 | } 718 | def get_all_data(conn) do 719 | pid = get_cache_pid(conn) 720 | 721 | %{ 722 | login_time_stamp: return_key(pid, :kinde_login_time_stamp), 723 | expiring_time_stamp: return_key(pid, :kinde_expiring_time_stamp), 724 | access_token: return_key(pid, :kinde_access_token), 725 | refresh_token: return_key(pid, :kinde_refresh_token), 726 | id_token: return_key(pid, :kinde_id_token), 727 | expires_in: return_key(pid, :kinde_expires_in), 728 | token: return_key(pid, :kinde_token), 729 | user: return_key(pid, :kinde_user), 730 | oauth_code_verifier: return_key(pid, :kinde_oauth_code_verifier) 731 | } 732 | end 733 | 734 | @doc """ 735 | Returns more readible version of any feature-flag 736 | 737 | ### Returns 738 | 739 | feature-flag map such as 740 | 741 | %{ 742 | "code" => "theme", 743 | "is_default" => false, 744 | "type" => "string", 745 | "value" => "grayscale" 746 | } 747 | 748 | ### Usage 749 | 750 | KindeClientSDK.get_flag(conn, "theme") 751 | KindeClientSDK.get_flag(conn, "theme", "black") 752 | KindeClientSDK.get_flag(conn, "theme", "black", "s") 753 | """ 754 | @spec get_flag(Plug.Conn.t(), String.t()) :: map() | String.t() 755 | def get_flag(conn, code) do 756 | %{name: _claim_name, value: feature_flags} = KindeClientSDK.get_claim(conn, "feature_flags") 757 | 758 | case feature_flags[code] do 759 | nil -> 760 | "This flag does not exist, and no default value provided" 761 | 762 | %{"t" => flag_type, "v" => value} -> 763 | %{ 764 | "code" => code, 765 | "type" => get_type(flag_type), 766 | "value" => value, 767 | "is_default" => false 768 | } 769 | end 770 | end 771 | 772 | @spec get_flag(Plug.Conn.t(), String.t(), any()) :: map() | String.t() 773 | def get_flag(conn, code, default_value) do 774 | %{name: _claim_name, value: feature_flags} = KindeClientSDK.get_claim(conn, "feature_flags") 775 | 776 | case feature_flags[code] do 777 | nil -> 778 | %{ 779 | "code" => code, 780 | "value" => default_value, 781 | "is_default" => true 782 | } 783 | 784 | _ -> 785 | get_flag(conn, code) 786 | end 787 | end 788 | 789 | @spec get_flag(Plug.Conn.t(), String.t(), any(), String.t()) :: map() | String.t() 790 | def get_flag(conn, code, default_value, flag_type) do 791 | %{name: _claim_name, value: feature_flags} = KindeClientSDK.get_claim(conn, "feature_flags") 792 | 793 | case feature_flags[code] do 794 | %{"t" => actual_type} when actual_type != flag_type -> 795 | "The flag type was provided as #{get_type(flag_type)}, but it is #{get_type(actual_type)}" 796 | 797 | _ -> 798 | get_flag(conn, code, default_value) 799 | end 800 | end 801 | 802 | @doc """ 803 | Returns a boolean flag from feature-flags object 804 | 805 | ### Returns 806 | 807 | true, false or error-messages 808 | 809 | ### Usage 810 | 811 | KindeClientSDK.get_boolean_flag(conn, "is_dark_mode") 812 | KindeClientSDK.get_boolean_flag(conn, "is_dark_mode", false) 813 | """ 814 | @spec get_boolean_flag(Plug.Conn.t(), String.t()) :: boolean() | String.t() 815 | def get_boolean_flag(conn, code) do 816 | %{name: _claim_name, value: feature_flags} = KindeClientSDK.get_claim(conn, "feature_flags") 817 | 818 | case feature_flags[code] do 819 | %{"t" => "b", "v" => value} -> 820 | value 821 | 822 | %{"t" => type} -> 823 | "Error - Flag #{code} is of type #{get_type(type)} not boolean" 824 | 825 | _ -> 826 | "Error - flag does not exist and no default provided" 827 | end 828 | end 829 | 830 | @spec get_boolean_flag(Plug.Conn.t(), String.t(), boolean()) :: boolean() | String.t() 831 | def get_boolean_flag(conn, code, default_value) do 832 | %{name: _claim_name, value: feature_flags} = KindeClientSDK.get_claim(conn, "feature_flags") 833 | 834 | case feature_flags[code] do 835 | nil -> 836 | default_value 837 | 838 | %{"t" => "b", "v" => value} -> 839 | value 840 | 841 | %{"t" => type, "v" => _} -> 842 | "Error - Flag #{code} is of type #{get_type(type)} not boolean" 843 | end 844 | end 845 | 846 | @doc """ 847 | Returns a string flag from feature-flags object 848 | 849 | ### Returns 850 | 851 | corresponding values from object or error-messages 852 | 853 | ### Usage 854 | 855 | KindeClientSDK.get_string_flag(conn, "theme") 856 | KindeClientSDK.get_string_flag(conn, "theme", "black") 857 | """ 858 | @spec get_string_flag(Plug.Conn.t(), String.t()) :: String.t() 859 | def get_string_flag(conn, code) do 860 | %{name: _claim_name, value: feature_flags} = KindeClientSDK.get_claim(conn, "feature_flags") 861 | 862 | case feature_flags[code] do 863 | %{"t" => "s", "v" => value} -> 864 | value 865 | 866 | %{"t" => type} -> 867 | "Error - Flag #{code} is of type #{get_type(type)} not string" 868 | 869 | _ -> 870 | "Error - flag does not exist and no default provided" 871 | end 872 | end 873 | 874 | @spec get_string_flag(Plug.Conn.t(), String.t(), String.t()) :: String.t() 875 | def get_string_flag(conn, code, default_value) do 876 | %{name: _claim_name, value: feature_flags} = KindeClientSDK.get_claim(conn, "feature_flags") 877 | 878 | case feature_flags[code] do 879 | nil -> 880 | default_value 881 | 882 | %{"t" => "s", "v" => value} -> 883 | value 884 | 885 | %{"t" => type, "v" => _} -> 886 | "Error - Flag #{code} is of type #{get_type(type)} not string" 887 | end 888 | end 889 | 890 | @doc """ 891 | Returns a integer flag from feature-flags object 892 | 893 | ### Returns 894 | 895 | corresponding values from object or error-messages 896 | 897 | ### Usage 898 | 899 | KindeClientSDK.get_integer_flag(conn, "counter") 900 | KindeClientSDK.get_integer_flag(conn, "counter", 46) 901 | """ 902 | 903 | @spec get_integer_flag(Plug.Conn.t(), String.t()) :: integer() | String.t() 904 | def get_integer_flag(conn, code) do 905 | %{name: _claim_name, value: feature_flags} = KindeClientSDK.get_claim(conn, "feature_flags") 906 | 907 | case feature_flags[code] do 908 | %{"t" => "i", "v" => value} -> 909 | value 910 | 911 | %{"t" => type} -> 912 | "Error - Flag #{code} is of type #{get_type(type)} not integer" 913 | 914 | _ -> 915 | "Error - flag does not exist and no default provided" 916 | end 917 | end 918 | 919 | @spec get_integer_flag(Plug.Conn.t(), String.t(), integer()) :: integer() | String.t() 920 | def get_integer_flag(conn, code, default_value) do 921 | %{name: _claim_name, value: feature_flags} = KindeClientSDK.get_claim(conn, "feature_flags") 922 | 923 | case feature_flags[code] do 924 | nil -> 925 | default_value 926 | 927 | %{"t" => "i", "v" => value} -> 928 | value 929 | 930 | %{"t" => type, "v" => _} -> 931 | "Error - Flag #{code} is of type #{get_type(type)} not integer" 932 | end 933 | end 934 | 935 | defp get_type(flag) when is_map(flag) do 936 | type = flag["t"] 937 | 938 | case type do 939 | "i" -> 940 | "integer" 941 | 942 | "s" -> 943 | "string" 944 | 945 | "b" -> 946 | "boolean" 947 | 948 | _ -> 949 | "undefined" 950 | end 951 | end 952 | 953 | defp get_type(flag) do 954 | case flag do 955 | "i" -> 956 | "integer" 957 | 958 | "s" -> 959 | "string" 960 | 961 | "b" -> 962 | "boolean" 963 | 964 | _ -> 965 | "undefined" 966 | end 967 | end 968 | 969 | defp return_key(pid, key) do 970 | case GenServer.call(pid, {:get_kinde_data, key}) do 971 | [{_, data}] -> data 972 | _ -> nil 973 | end 974 | end 975 | end 976 | -------------------------------------------------------------------------------- /lib/kinde_sdk/model/add_organization_users_200_response.ex: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator 6.3.0 (https://openapi-generator.tech). 2 | # Do not edit this file manually. 3 | 4 | defmodule KindeSDK.Model.AddOrganizationUsers200Response do 5 | @moduledoc false 6 | 7 | @derive [Poison.Encoder] 8 | defstruct [ 9 | :message, 10 | :users_add 11 | ] 12 | 13 | @type t :: %__MODULE__{ 14 | :message => String.t() | nil, 15 | :users_add => [String.t()] | nil 16 | } 17 | end 18 | 19 | defimpl Poison.Decoder, for: KindeSDK.Model.AddOrganizationUsers200Response do 20 | def decode(value, _options) do 21 | value 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/kinde_sdk/model/add_organization_users_request.ex: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator 6.3.0 (https://openapi-generator.tech). 2 | # Do not edit this file manually. 3 | 4 | defmodule KindeSDK.Model.AddOrganizationUsersRequest do 5 | @moduledoc false 6 | 7 | @derive [Poison.Encoder] 8 | defstruct [ 9 | :users 10 | ] 11 | 12 | @type t :: %__MODULE__{ 13 | :users => [String.t()] | nil 14 | } 15 | end 16 | 17 | defimpl Poison.Decoder, for: KindeSDK.Model.AddOrganizationUsersRequest do 18 | def decode(value, _options) do 19 | value 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/kinde_sdk/model/create_organization_request.ex: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator 6.3.0 (https://openapi-generator.tech). 2 | # Do not edit this file manually. 3 | 4 | defmodule KindeSDK.Model.CreateOrganizationRequest do 5 | @moduledoc false 6 | 7 | @derive [Poison.Encoder] 8 | defstruct [ 9 | :name 10 | ] 11 | 12 | @type t :: %__MODULE__{ 13 | :name => String.t() | nil 14 | } 15 | end 16 | 17 | defimpl Poison.Decoder, for: KindeSDK.Model.CreateOrganizationRequest do 18 | def decode(value, _options) do 19 | value 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/kinde_sdk/model/create_user_200_response.ex: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator 6.3.0 (https://openapi-generator.tech). 2 | # Do not edit this file manually. 3 | 4 | defmodule KindeSDK.Model.CreateUser200Response do 5 | @moduledoc false 6 | 7 | @derive [Poison.Encoder] 8 | defstruct [ 9 | :id, 10 | :created, 11 | :identities 12 | ] 13 | 14 | @type t :: %__MODULE__{ 15 | :id => String.t() | nil, 16 | :created => boolean() | nil, 17 | :identities => [KindeSDK.Model.UserIdentity.t()] | nil 18 | } 19 | end 20 | 21 | defimpl Poison.Decoder, for: KindeSDK.Model.CreateUser200Response do 22 | import KindeSDK.Deserializer 23 | 24 | def decode(value, options) do 25 | value 26 | |> deserialize(:identities, :list, KindeSDK.Model.UserIdentity, options) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/kinde_sdk/model/create_user_request.ex: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator 6.3.0 (https://openapi-generator.tech). 2 | # Do not edit this file manually. 3 | 4 | defmodule KindeSDK.Model.CreateUserRequest do 5 | @moduledoc false 6 | 7 | @derive [Poison.Encoder] 8 | defstruct [ 9 | :profile, 10 | :identities 11 | ] 12 | 13 | @type t :: %__MODULE__{ 14 | :profile => KindeSDK.Model.CreateUserRequestProfile.t() | nil, 15 | :identities => [KindeSDK.Model.CreateUserRequestIdentitiesInner.t()] | nil 16 | } 17 | end 18 | 19 | defimpl Poison.Decoder, for: KindeSDK.Model.CreateUserRequest do 20 | import KindeSDK.Deserializer 21 | 22 | def decode(value, options) do 23 | value 24 | |> deserialize(:profile, :struct, KindeSDK.Model.CreateUserRequestProfile, options) 25 | |> deserialize(:identities, :list, KindeSDK.Model.CreateUserRequestIdentitiesInner, options) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/kinde_sdk/model/create_user_request_identities_inner.ex: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator 6.3.0 (https://openapi-generator.tech). 2 | # Do not edit this file manually. 3 | 4 | defmodule KindeSDK.Model.CreateUserRequestIdentitiesInner do 5 | @moduledoc """ 6 | The result of the user creation operation 7 | """ 8 | 9 | @derive [Poison.Encoder] 10 | defstruct [ 11 | :type, 12 | :details 13 | ] 14 | 15 | @type t :: %__MODULE__{ 16 | :type => String.t() | nil, 17 | :details => KindeSDK.Model.CreateUserRequestIdentitiesInnerDetails.t() | nil 18 | } 19 | end 20 | 21 | defimpl Poison.Decoder, for: KindeSDK.Model.CreateUserRequestIdentitiesInner do 22 | import KindeSDK.Deserializer 23 | 24 | def decode(value, options) do 25 | value 26 | |> deserialize( 27 | :details, 28 | :struct, 29 | KindeSDK.Model.CreateUserRequestIdentitiesInnerDetails, 30 | options 31 | ) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/kinde_sdk/model/create_user_request_identities_inner_details.ex: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator 6.3.0 (https://openapi-generator.tech). 2 | # Do not edit this file manually. 3 | 4 | defmodule KindeSDK.Model.CreateUserRequestIdentitiesInnerDetails do 5 | @moduledoc """ 6 | Additional details required to create the user 7 | """ 8 | 9 | @derive [Poison.Encoder] 10 | defstruct [ 11 | :email 12 | ] 13 | 14 | @type t :: %__MODULE__{ 15 | :email => String.t() | nil 16 | } 17 | end 18 | 19 | defimpl Poison.Decoder, for: KindeSDK.Model.CreateUserRequestIdentitiesInnerDetails do 20 | def decode(value, _options) do 21 | value 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/kinde_sdk/model/create_user_request_profile.ex: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator 6.3.0 (https://openapi-generator.tech). 2 | # Do not edit this file manually. 3 | 4 | defmodule KindeSDK.Model.CreateUserRequestProfile do 5 | @moduledoc """ 6 | Basic information required to create a user 7 | """ 8 | 9 | @derive [Poison.Encoder] 10 | defstruct [ 11 | :given_name, 12 | :family_name 13 | ] 14 | 15 | @type t :: %__MODULE__{ 16 | :given_name => String.t() | nil, 17 | :family_name => String.t() | nil 18 | } 19 | end 20 | 21 | defimpl Poison.Decoder, for: KindeSDK.Model.CreateUserRequestProfile do 22 | def decode(value, _options) do 23 | value 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/kinde_sdk/model/organization.ex: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator 6.3.0 (https://openapi-generator.tech). 2 | # Do not edit this file manually. 3 | 4 | defmodule KindeSDK.Model.Organization do 5 | @moduledoc false 6 | 7 | @derive [Poison.Encoder] 8 | defstruct [ 9 | :code, 10 | :name, 11 | :is_default 12 | ] 13 | 14 | @type t :: %__MODULE__{ 15 | :code => String.t() | nil, 16 | :name => String.t() | nil, 17 | :is_default => boolean() | nil 18 | } 19 | end 20 | 21 | defimpl Poison.Decoder, for: KindeSDK.Model.Organization do 22 | def decode(value, _options) do 23 | value 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/kinde_sdk/model/organization_user.ex: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator 6.3.0 (https://openapi-generator.tech). 2 | # Do not edit this file manually. 3 | 4 | defmodule KindeSDK.Model.OrganizationUser do 5 | @moduledoc false 6 | 7 | @derive [Poison.Encoder] 8 | defstruct [ 9 | :user_id, 10 | :email, 11 | :full_name, 12 | :last_name, 13 | :first_name 14 | ] 15 | 16 | @type t :: %__MODULE__{ 17 | :user_id => integer() | nil, 18 | :email => String.t() | nil, 19 | :full_name => String.t() | nil, 20 | :last_name => String.t() | nil, 21 | :first_name => String.t() | nil 22 | } 23 | end 24 | 25 | defimpl Poison.Decoder, for: KindeSDK.Model.OrganizationUser do 26 | def decode(value, _options) do 27 | value 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/kinde_sdk/model/remove_organization_users_200_response.ex: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator 6.3.0 (https://openapi-generator.tech). 2 | # Do not edit this file manually. 3 | 4 | defmodule KindeSDK.Model.RemoveOrganizationUsers200Response do 5 | @moduledoc false 6 | 7 | @derive [Poison.Encoder] 8 | defstruct [ 9 | :message, 10 | :users_added 11 | ] 12 | 13 | @type t :: %__MODULE__{ 14 | :message => String.t() | nil, 15 | :users_added => [String.t()] | nil 16 | } 17 | end 18 | 19 | defimpl Poison.Decoder, for: KindeSDK.Model.RemoveOrganizationUsers200Response do 20 | def decode(value, _options) do 21 | value 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/kinde_sdk/model/remove_organization_users_request.ex: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator 6.3.0 (https://openapi-generator.tech). 2 | # Do not edit this file manually. 3 | 4 | defmodule KindeSDK.Model.RemoveOrganizationUsersRequest do 5 | @moduledoc false 6 | 7 | @derive [Poison.Encoder] 8 | defstruct [ 9 | :users 10 | ] 11 | 12 | @type t :: %__MODULE__{ 13 | :users => [String.t()] | nil 14 | } 15 | end 16 | 17 | defimpl Poison.Decoder, for: KindeSDK.Model.RemoveOrganizationUsersRequest do 18 | def decode(value, _options) do 19 | value 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/kinde_sdk/model/update_user_request.ex: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator 6.3.0 (https://openapi-generator.tech). 2 | # Do not edit this file manually. 3 | 4 | defmodule KindeSDK.Model.UpdateUserRequest do 5 | @moduledoc false 6 | 7 | @derive [Poison.Encoder] 8 | defstruct [ 9 | :given_name, 10 | :family_name 11 | ] 12 | 13 | @type t :: %__MODULE__{ 14 | :given_name => String.t() | nil, 15 | :family_name => String.t() | nil 16 | } 17 | end 18 | 19 | defimpl Poison.Decoder, for: KindeSDK.Model.UpdateUserRequest do 20 | def decode(value, _options) do 21 | value 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/kinde_sdk/model/user.ex: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator 6.3.0 (https://openapi-generator.tech). 2 | # Do not edit this file manually. 3 | 4 | defmodule KindeSDK.Model.User do 5 | @moduledoc false 6 | 7 | @derive [Poison.Encoder] 8 | defstruct [ 9 | :id, 10 | :email, 11 | :last_name, 12 | :first_name, 13 | :is_suspended 14 | ] 15 | 16 | @type t :: %__MODULE__{ 17 | :id => integer() | nil, 18 | :email => String.t() | nil, 19 | :last_name => String.t() | nil, 20 | :first_name => String.t() | nil, 21 | :is_suspended => boolean() | nil 22 | } 23 | end 24 | 25 | defimpl Poison.Decoder, for: KindeSDK.Model.User do 26 | def decode(value, _options) do 27 | value 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/kinde_sdk/model/user_identity.ex: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator 6.3.0 (https://openapi-generator.tech). 2 | # Do not edit this file manually. 3 | 4 | defmodule KindeSDK.Model.UserIdentity do 5 | @moduledoc false 6 | 7 | @derive [Poison.Encoder] 8 | defstruct [ 9 | :type, 10 | :result 11 | ] 12 | 13 | @type t :: %__MODULE__{ 14 | :type => String.t() | nil, 15 | :result => KindeSDK.Model.UserIdentityResult.t() | nil 16 | } 17 | end 18 | 19 | defimpl Poison.Decoder, for: KindeSDK.Model.UserIdentity do 20 | import KindeSDK.Deserializer 21 | 22 | def decode(value, options) do 23 | value 24 | |> deserialize(:result, :struct, KindeSDK.Model.UserIdentityResult, options) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/kinde_sdk/model/user_identity_result.ex: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator 6.3.0 (https://openapi-generator.tech). 2 | # Do not edit this file manually. 3 | 4 | defmodule KindeSDK.Model.UserIdentityResult do 5 | @moduledoc """ 6 | The result of the user creation operation 7 | """ 8 | 9 | @derive [Poison.Encoder] 10 | defstruct [ 11 | :created, 12 | :identity_id 13 | ] 14 | 15 | @type t :: %__MODULE__{ 16 | :created => boolean() | nil, 17 | :identity_id => integer() | nil 18 | } 19 | end 20 | 21 | defimpl Poison.Decoder, for: KindeSDK.Model.UserIdentityResult do 22 | def decode(value, _options) do 23 | value 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/kinde_sdk/model/user_profile.ex: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator 6.3.0 (https://openapi-generator.tech). 2 | # Do not edit this file manually. 3 | 4 | defmodule KindeSDK.Model.UserProfile do 5 | @moduledoc false 6 | 7 | @derive [Poison.Encoder] 8 | defstruct [ 9 | :id, 10 | :preferred_email, 11 | :provided_id, 12 | :last_name, 13 | :first_name 14 | ] 15 | 16 | @type t :: %__MODULE__{ 17 | :id => String.t() | nil, 18 | :preferred_email => String.t() | nil, 19 | :provided_id => String.t() | nil, 20 | :last_name => String.t() | nil, 21 | :first_name => String.t() | nil 22 | } 23 | end 24 | 25 | defimpl Poison.Decoder, for: KindeSDK.Model.UserProfile do 26 | def decode(value, _options) do 27 | value 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/kinde_sdk/model/user_profile_v2.ex: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator 6.3.0 (https://openapi-generator.tech). 2 | # Do not edit this file manually. 3 | 4 | defmodule KindeSDK.Model.UserProfileV2 do 5 | @moduledoc false 6 | 7 | @derive [Poison.Encoder] 8 | defstruct [ 9 | :id, 10 | :provided_id, 11 | :name, 12 | :given_name, 13 | :family_name, 14 | :updated_at 15 | ] 16 | 17 | @type t :: %__MODULE__{ 18 | :id => String.t() | nil, 19 | :provided_id => String.t() | nil, 20 | :name => String.t() | nil, 21 | :given_name => String.t() | nil, 22 | :family_name => String.t() | nil, 23 | :updated_at => String.t() | nil 24 | } 25 | end 26 | 27 | defimpl Poison.Decoder, for: KindeSDK.Model.UserProfileV2 do 28 | def decode(value, _options) do 29 | value 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/kinde_sdk/request_builder.ex: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator 6.3.0 (https://openapi-generator.tech). 2 | # Do not edit this file manually. 3 | 4 | defmodule KindeSDK.RequestBuilder do 5 | @moduledoc """ 6 | Helper functions for building Tesla requests 7 | """ 8 | 9 | @doc """ 10 | Specify the request `method` when building a request. 11 | 12 | Does not override the `method` if one has already been specified. 13 | 14 | ### Parameters 15 | 16 | - `request` (Map) - Collected request options 17 | - `method` (atom) - Request method 18 | 19 | ### Returns 20 | 21 | Map 22 | """ 23 | @spec method(map(), atom()) :: map() 24 | def method(request, method) do 25 | Map.put_new(request, :method, method) 26 | end 27 | 28 | @doc """ 29 | Specify the request URL when building a request. 30 | 31 | Does not override the `url` if one has already been specified. 32 | 33 | ### Parameters 34 | 35 | - `request` (Map) - Collected request options 36 | - `url` (String) - Request URL 37 | 38 | ### Returns 39 | 40 | Map 41 | """ 42 | @spec url(map(), String.t()) :: map() 43 | def url(request, url) do 44 | Map.put_new(request, :url, url) 45 | end 46 | 47 | @doc """ 48 | Add optional parameters to the request. 49 | 50 | ### Parameters 51 | 52 | - `request` (Map) - Collected request options 53 | - `definitions` (Map) - Map of parameter name to parameter location. 54 | - `options` (KeywordList) - The provided optional parameters 55 | 56 | ### Returns 57 | 58 | Map 59 | """ 60 | @spec add_optional_params(map(), %{optional(atom) => atom()}, keyword()) :: map() 61 | def add_optional_params(request, _, []), do: request 62 | 63 | def add_optional_params(request, definitions, [{key, value} | tail]) do 64 | case definitions do 65 | %{^key => location} -> 66 | request 67 | |> add_param(location, key, value) 68 | |> add_optional_params(definitions, tail) 69 | 70 | _ -> 71 | add_optional_params(request, definitions, tail) 72 | end 73 | end 74 | 75 | @doc """ 76 | Add non-optional parameters to the request. 77 | 78 | ### Parameters 79 | 80 | - `request` (Map) - Collected request options 81 | - `location` (atom) - Where to put the parameter 82 | - `key` (atom) - The name of the parameter 83 | - `value` (any) - The value of the parameter 84 | 85 | ### Returns 86 | 87 | Map 88 | """ 89 | @spec add_param(map(), atom(), atom(), any()) :: map() 90 | def add_param(request, :body, :body, value), do: Map.put(request, :body, value) 91 | 92 | def add_param(request, :body, key, value) do 93 | request 94 | |> Map.put_new_lazy(:body, &Tesla.Multipart.new/0) 95 | |> Map.update!(:body, fn multipart -> 96 | Tesla.Multipart.add_field( 97 | multipart, 98 | key, 99 | Poison.encode!(value), 100 | headers: [{:"Content-Type", "application/json"}] 101 | ) 102 | end) 103 | end 104 | 105 | def add_param(request, :headers, key, value) do 106 | headers = 107 | request 108 | |> Map.get(:headers, []) 109 | |> List.keystore(key, 0, {key, value}) 110 | 111 | Map.put(request, :headers, headers) 112 | end 113 | 114 | def add_param(request, :file, name, path) do 115 | request 116 | |> Map.put_new_lazy(:body, &Tesla.Multipart.new/0) 117 | |> Map.update!(:body, &Tesla.Multipart.add_file(&1, path, name: name)) 118 | end 119 | 120 | def add_param(request, :form, name, value) do 121 | Map.update(request, :body, %{name => value}, &Map.put(&1, name, value)) 122 | end 123 | 124 | def add_param(request, location, key, value) do 125 | Map.update(request, location, [{key, value}], &(&1 ++ [{key, value}])) 126 | end 127 | 128 | @doc """ 129 | This function ensures that the `body` parameter is always set. 130 | 131 | When using Tesla with the `httpc` adapter (the default adapter), there is a 132 | bug where POST, PATCH and PUT requests will fail if the body is empty. 133 | 134 | ### Parameters 135 | 136 | - `request` (Map) - Collected request options 137 | 138 | ### Returns 139 | 140 | Map 141 | """ 142 | @spec ensure_body(map()) :: map() 143 | def ensure_body(%{body: nil} = request) do 144 | %{request | body: ""} 145 | end 146 | 147 | def ensure_body(request) do 148 | Map.put_new(request, :body, "") 149 | end 150 | 151 | @type status_code :: 100..599 152 | @type response_mapping :: [{status_code, struct() | false}] 153 | 154 | @doc """ 155 | Evaluate the response from a Tesla request. 156 | Decode the response for a Tesla request. 157 | 158 | ### Parameters 159 | 160 | - `result` (Tesla.Env.result()): The response from Tesla.request/2. 161 | - `mapping` ([{http_status, struct}]): The mapping for status to struct for decoding. 162 | 163 | ### Returns 164 | 165 | - `{:ok, struct}` or `{:ok, Tesla.Env.t()}` on success 166 | - `{:error, term}` on failure 167 | """ 168 | @spec evaluate_response(Tesla.Env.result(), response_mapping) :: 169 | {:ok, struct()} | Tesla.Env.result() 170 | def evaluate_response({:ok, %Tesla.Env{} = env}, mapping) do 171 | resolve_mapping(env, mapping, nil) 172 | end 173 | 174 | def evaluate_response({:error, _} = error, _), do: error 175 | 176 | defp resolve_mapping(%Tesla.Env{status: status} = env, [{mapping_status, struct} | _], _) 177 | when status == mapping_status do 178 | decode(env, struct) 179 | end 180 | 181 | defp resolve_mapping(env, [{:default, struct} | tail], _), 182 | do: resolve_mapping(env, tail, struct) 183 | 184 | defp resolve_mapping(env, [_ | tail], struct), do: resolve_mapping(env, tail, struct) 185 | 186 | defp resolve_mapping(env, [], nil), do: {:error, env} 187 | 188 | defp resolve_mapping(env, [], struct), do: decode(env, struct) 189 | 190 | defp decode(%Tesla.Env{} = env, false), do: {:ok, env} 191 | 192 | defp decode(%Tesla.Env{body: body}, struct), do: Poison.decode(body, as: struct) 193 | end 194 | -------------------------------------------------------------------------------- /lib/kinde_sdk/sdk/authorization_code.ex: -------------------------------------------------------------------------------- 1 | defmodule KindeSDK.SDK.AuthorizationCode do 2 | @moduledoc """ 3 | Authorization Code OAuth2 Flow 4 | 5 | This module provides functions for implementing the Authorization Code OAuth2 flow in the Kinde SDK. The Authorization Code flow is commonly used for user authentication, allowing a Kinde application to obtain an access token after the user authorizes the application. 6 | 7 | ## Usage Example 8 | 9 | To initiate an Authorization Code OAuth2 flow, you can call the `login/3` function as follows: 10 | 11 | ```elixir 12 | conn = KindeSDK.SDK.AuthorizationCode.login(conn, client, additional_params) 13 | 14 | This module simplifies the process of initiating the Authorization Code flow for user authentication in Kinde applications. 15 | """ 16 | alias KindeSDK.SDK.Utils 17 | 18 | @spec login( 19 | Plug.Conn.t(), 20 | %{ 21 | :additional_params => map, 22 | :authorization_endpoint => any, 23 | :cache_pid => atom | pid | {atom, any} | {:via, atom, any}, 24 | :client_id => any, 25 | :redirect_uri => any, 26 | :scopes => any, 27 | optional(any) => any 28 | }, 29 | map 30 | ) :: Plug.Conn.t() 31 | def login(conn, client, additional_params \\ %{}) do 32 | state = Utils.random_string() 33 | 34 | GenServer.cast(client.cache_pid, {:add_kinde_data, {:kinde_oauth_state, state}}) 35 | 36 | search_params = %{ 37 | client_id: client.client_id, 38 | grant_type: :authorization_code, 39 | redirect_uri: client.redirect_uri, 40 | response_type: :code, 41 | scope: client.scopes, 42 | state: state, 43 | start_page: :login 44 | } 45 | 46 | params = 47 | Utils.add_additional_params(client.additional_params, additional_params) 48 | |> Map.merge(search_params) 49 | |> URI.encode_query() 50 | 51 | conn 52 | |> Plug.Conn.resp(:found, "") 53 | |> Plug.Conn.put_resp_header("location", "#{client.authorization_endpoint}?#{params}") 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/kinde_sdk/sdk/client_credentials.ex: -------------------------------------------------------------------------------- 1 | defmodule KindeSDK.SDK.ClientCredentials do 2 | @moduledoc """ 3 | Client Credentials OAuth2 Flow 4 | 5 | This module provides functions for implementing the Client Credentials OAuth2 flow in the Kinde SDK. The Client Credentials flow is typically used for server-to-server communication, allowing a Kinde application to obtain access tokens and access secured resources. 6 | 7 | ## Usage Example 8 | 9 | To acquire an access token using the Client Credentials flow, you can call the `login/3` function as follows: 10 | 11 | ```elixir 12 | token = KindeSDK.SDK.ClientCredentials.login(client, additional_params) 13 | 14 | This module simplifies the process of obtaining an access token for server-side authentication in Kinde applications. 15 | """ 16 | alias KindeSDK.SDK.Utils 17 | use Tesla 18 | alias HTTPoison 19 | 20 | @spec login( 21 | any, 22 | %{ 23 | :additional_params => map, 24 | :cache_pid => atom | pid | {atom, any} | {:via, atom, any}, 25 | :client_id => any, 26 | :client_secret => any, 27 | :scopes => any, 28 | :token_endpoint => binary, 29 | optional(any) => any 30 | }, 31 | map 32 | ) :: any 33 | def login(conn, client, additional_params \\ %{}) do 34 | form_data = %{ 35 | client_id: client.client_id, 36 | client_secret: client.client_secret, 37 | grant_type: :client_credentials, 38 | scope: client.scopes 39 | } 40 | 41 | params = 42 | Utils.add_additional_params(client.additional_params, additional_params) 43 | |> Map.merge(form_data) 44 | |> Map.to_list() 45 | 46 | body = {:form, params} 47 | 48 | {:ok, response} = 49 | HTTPoison.post(client.token_endpoint, body, [ 50 | {"Kinde-SDK", "Elixir/#{Utils.get_current_app_version()}"} 51 | ]) 52 | 53 | contents = Jason.decode!(response.body) 54 | 55 | GenServer.cast( 56 | client.cache_pid, 57 | {:add_kinde_data, {:kinde_access_token, contents["access_token"]}} 58 | ) 59 | 60 | expires_in = if is_nil(contents["expires_in"]), do: 0, else: contents["expires_in"] 61 | 62 | GenServer.cast( 63 | client.cache_pid, 64 | {:add_kinde_data, {:kinde_expires_in, expires_in}} 65 | ) 66 | 67 | GenServer.cast( 68 | client.cache_pid, 69 | {:add_kinde_data, {:kinde_login_time_stamp, DateTime.utc_now()}} 70 | ) 71 | 72 | GenServer.cast(client.cache_pid, {:add_kinde_data, {:kinde_token, contents}}) 73 | 74 | conn 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/kinde_sdk/sdk/pkce.ex: -------------------------------------------------------------------------------- 1 | defmodule KindeSDK.SDK.Pkce do 2 | alias KindeSDK.SDK.Utils 3 | 4 | @moduledoc """ 5 | PKCE (Proof Key for Code Exchange) OAuth2 Login Flow 6 | 7 | This module provides functions for implementing the PKCE OAuth2 login flow 8 | in the Kinde SDK. PKCE is a security extension for OAuth2 to protect against 9 | code interception and misuse. This module facilitates the login process and the 10 | generation of PKCE code challenges. 11 | 12 | ## Usage Example 13 | 14 | To initiate a PKCE OAuth2 login flow, you can call the `login/4` function as follows: 15 | 16 | ```elixir 17 | conn = KindeSDK.SDK.Pkce.login(conn, client, start_page, additional_params) 18 | 19 | This module is designed to simplify the login process while enhancing security in Kinde applications. 20 | """ 21 | @spec login( 22 | Plug.Conn.t(), 23 | %{ 24 | :additional_params => map, 25 | :authorization_endpoint => any, 26 | :cache_pid => atom | pid | {atom, any} | {:via, atom, any}, 27 | :client_id => any, 28 | :redirect_uri => any, 29 | :scopes => any, 30 | optional(any) => any 31 | }, 32 | any, 33 | map 34 | ) :: Plug.Conn.t() 35 | def login(conn, client, start_page, additional_params) do 36 | GenServer.cast(client.cache_pid, {:add_kinde_data, {:kinde_oauth_code_verifier, nil}}) 37 | challenge = Utils.generate_challenge() 38 | 39 | GenServer.cast(client.cache_pid, {:add_kinde_data, {:kinde_oauth_state, challenge.state}}) 40 | 41 | search_params = %{ 42 | redirect_uri: client.redirect_uri, 43 | client_id: client.client_id, 44 | response_type: :code, 45 | scope: client.scopes, 46 | code_challenge: challenge.code_challenge, 47 | code_verifier: challenge.code_verifier, 48 | code_challenge_method: "S256", 49 | state: challenge.state, 50 | start_page: start_page 51 | } 52 | 53 | params = 54 | Utils.add_additional_params(client.additional_params, additional_params) 55 | |> Map.merge(search_params) 56 | |> URI.encode_query() 57 | 58 | GenServer.cast( 59 | client.cache_pid, 60 | {:add_kinde_data, {:kinde_oauth_code_verifier, challenge.code_verifier}} 61 | ) 62 | 63 | conn 64 | |> Plug.Conn.resp(:found, "") 65 | |> Plug.Conn.put_resp_header("location", "#{client.authorization_endpoint}?#{params}") 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/kinde_sdk/sdk/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule KindeSDK.SDK.Utils do 2 | @moduledoc """ 3 | Utility Functions for Kinde SDK 4 | 5 | This module provides a collection of utility functions used in the Kinde Software 6 | Development Kit (SDK). These functions cover a range of tasks such as encoding and 7 | decoding data, generating random values, validating URLs, parsing JSON Web Tokens (JWTs), and more. 8 | 9 | ## Usage Example 10 | 11 | To generate a challenge for OAuth2 authorization code flow, you can use the 12 | `generate_challenge/0` function as follows: 13 | 14 | ```elixir 15 | challenge = KindeSDK.SDK.Utils.generate_challenge() 16 | ``` 17 | This module is an essential part of the Kinde SDK and is designed to simplify 18 | common tasks associated with Kinde application development. 19 | """ 20 | @length 32 21 | 22 | ## Encodes a string as a Base64 URL-safe string. 23 | ## This function takes a binary string and encodes it as a Base64 URL-safe string. 24 | ## It removes padding characters ('=') and replaces '+' with '-' and '/' with '_'. 25 | defp base64_url_encode(string) do 26 | string 27 | |> Base.url_encode64() 28 | |> String.trim() 29 | |> String.replace("=", "") 30 | |> String.replace("+", "-") 31 | |> String.replace("/", "_") 32 | end 33 | 34 | @doc """ 35 | Generate a random string of a specified length. 36 | 37 | This function generates a random binary string of the specified length and 38 | encodes it as a Base64 URL-safe string. 39 | 40 | ## Parameters 41 | 42 | - `length` (integer): The length of the random string. (default: 32) 43 | 44 | ## Returns 45 | 46 | A Base64 URL-safe encoded binary string. 47 | """ 48 | @spec random_string(any) :: binary 49 | def random_string(length \\ @length) do 50 | :crypto.strong_rand_bytes(length) 51 | |> Base.encode16() 52 | |> base64_url_encode() 53 | end 54 | 55 | @doc """ 56 | Generate a challenge for OAuth2 authorization code flow. 57 | 58 | This function generates a challenge for OAuth2 authorization code flow. 59 | It includes a random `state`, a random `code_verifier`, and a `code_challenge` 60 | generated by hashing the `code_verifier` with SHA-256 and encoding it as a Base64 URL-safe string. 61 | 62 | ## Returns 63 | 64 | A map containing `code_challenge`, `code_verifier`, and `state`. 65 | """ 66 | @spec generate_challenge :: %{code_challenge: binary, code_verifier: binary, state: binary} 67 | def generate_challenge do 68 | state = random_string() 69 | code_verifier = random_string() 70 | code_challenge = :crypto.hash(:sha256, code_verifier) |> Base.url_encode64(padding: false) 71 | 72 | %{ 73 | state: state, 74 | code_verifier: code_verifier, 75 | code_challenge: code_challenge 76 | } 77 | end 78 | 79 | @doc """ 80 | Validate a URL. 81 | 82 | This function validates whether a given binary string is a valid URL using a regular expression. 83 | 84 | ## Parameters 85 | 86 | - `url` (binary): The URL to be validated. 87 | 88 | ## Returns 89 | 90 | `true` if the URL is valid, `false` otherwise. 91 | """ 92 | @spec validate_url(binary) :: boolean 93 | def validate_url(url) do 94 | Regex.match?( 95 | ~r/https?:\/\/(?:w{1,3}\.)?[^\s.]+(?:\.[a-z]+)*(?::\d+)?(?![^<]*(?:<\/\w+>|\/?>))/, 96 | url 97 | ) 98 | end 99 | 100 | @doc """ 101 | Parse a JSON Web Token (JWT). 102 | 103 | This function takes a JWT as a binary string and parses it into a map. 104 | 105 | ## Parameters 106 | 107 | - `token` (nil | binary): The JWT token to be parsed. 108 | 109 | ## Returns 110 | 111 | A map representing the JWT payload, or `nil` if the input is `nil`. 112 | """ 113 | @spec parse_jwt(nil | binary) :: any 114 | def parse_jwt(nil), do: nil 115 | 116 | def parse_jwt(token) do 117 | String.split(token, ".") 118 | |> Enum.at(1) 119 | |> String.replace("-", "+") 120 | |> String.replace("_", "/") 121 | |> Base.decode64!(padding: false) 122 | |> Jason.decode!() 123 | end 124 | 125 | @additional_param_keys [:audience, :org_code, :org_name, :is_create_org] 126 | 127 | @doc """ 128 | Check and filter additional parameters. 129 | 130 | This function checks a map of parameters to ensure they match the predefined 131 | additional parameter keys and are of binary type. It returns a map containing 132 | only the valid additional parameters. 133 | 134 | ## Parameters 135 | 136 | - `params` (map): The map of parameters to check. 137 | 138 | ## Returns 139 | 140 | A filtered map containing only valid additional parameters. 141 | """ 142 | @spec check_additional_params(map) :: map 143 | def check_additional_params(params) when params == %{}, do: %{} 144 | 145 | def check_additional_params(params) do 146 | keys = Map.keys(params) 147 | 148 | for key <- keys do 149 | if !(key in @additional_param_keys) do 150 | throw("Please provide correct additional, #{key}") 151 | end 152 | 153 | if !is_binary(Map.get(params, key)) do 154 | throw("Please supply a valid #{key}. Expected: string") 155 | end 156 | end 157 | 158 | params 159 | end 160 | 161 | @doc """ 162 | Add additional parameters to a target map. 163 | 164 | This function takes a target map and additional parameters, checks and 165 | filters the additional parameters, and merges them into the target map. 166 | 167 | ## Parameters 168 | 169 | - `target` (map): The target map to which additional parameters will be added. 170 | - `additional_params` (map): The map of additional parameters to be added. 171 | 172 | ## Returns 173 | 174 | A map containing the merged target map with valid additional parameters. 175 | """ 176 | @spec add_additional_params(map, map) :: map 177 | def add_additional_params(target, additional_params) do 178 | additional_params 179 | |> check_additional_params() 180 | |> Map.merge(target) 181 | end 182 | 183 | @doc """ 184 | Get the current application version from Mix configuration. 185 | 186 | This function retrieves the current application version from Mix 187 | configuration and performs some string manipulation to extract the version number. 188 | 189 | ## Returns 190 | 191 | The current application version as a binary string. 192 | """ 193 | @spec get_current_app_version :: binary 194 | def get_current_app_version() do 195 | # Using Application.spec(:vsn) to fetch the application version, suitable for production environments. 196 | :kinde_sdk 197 | |> Application.spec(:vsn) 198 | |> to_string() 199 | end 200 | 201 | @doc """ 202 | Calculate the timestamp when a login will expire. 203 | 204 | This function takes a login timestamp and the duration of validity, 205 | and calculates the timestamp when the login will expire. 206 | 207 | ## Parameters 208 | 209 | - `login_timestamp` (DateTime): The timestamp of the login. 210 | - `expiring_in` (integer): The duration of validity in seconds. 211 | 212 | ## Returns 213 | 214 | The timestamp when the login will expire. 215 | """ 216 | def calculate_expiring_timestamp(login_timestamp, expiring_in) do 217 | {:ok, expiring_at} = 218 | login_timestamp 219 | |> DateTime.to_unix() 220 | |> Kernel.+(expiring_in) 221 | |> DateTime.from_unix() 222 | 223 | expiring_at 224 | end 225 | end 226 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule KindeSDK.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :kinde_sdk, 7 | version: "1.2.0", 8 | elixir: "~> 1.10", 9 | build_embedded: Mix.env() == :prod, 10 | start_permanent: Mix.env() == :prod, 11 | package: package(), 12 | aliases: aliases(), 13 | description: "Provides endpoints to manage your Kinde Businesses", 14 | deps: deps() 15 | ] 16 | end 17 | 18 | # Configuration for the OTP application 19 | # 20 | # Type "mix help compile.app" for more information 21 | def application do 22 | # Specify extra applications you'll use from Erlang/Elixir 23 | [extra_applications: [:logger]] 24 | end 25 | 26 | # Dependencies can be Hex packages: 27 | # 28 | # {:my_dep, "~> 0.3.0"} 29 | # 30 | # Or git/path repositories: 31 | # 32 | # {:my_dep, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.3.0"} 33 | # 34 | # Type "mix help deps" for more examples and options 35 | defp deps do 36 | [ 37 | {:tesla, "~> 1.4"}, 38 | {:poison, "~> 3.0"}, 39 | {:ex_doc, "~> 0.28", only: :dev, runtime: false}, 40 | {:plug, "~> 1.13"}, 41 | {:plug_cowboy, "~> 2.0"}, 42 | {:jason, "~> 1.3"}, 43 | {:httpoison, "~> 0.7"}, 44 | {:envar, "~> 1.1.0"}, 45 | {:mock, "~> 0.3.0", only: :test}, 46 | {:ssl_verify_fun, "~> 1.1.7"}, 47 | {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, 48 | {:dialyxir, "~> 1.3", only: [:dev, :test], runtime: false} 49 | ] 50 | end 51 | 52 | defp package do 53 | [ 54 | name: "kinde_sdk", 55 | files: ~w(config lib test .formatter.exs .gitignore LICENSE* mix.exs README*), 56 | licenses: ["MIT"], 57 | links: %{"GitHub" => "https://github.com/kinde-oss/kinde-elixir-sdk"} 58 | ] 59 | end 60 | 61 | # Aliases are shortcuts or tasks specific to the current project. 62 | # For example, to install project dependencies and perform other setup tasks, run: 63 | # 64 | # $ mix setup 65 | # 66 | # See the documentation for `Mix` for more info on aliases. 67 | defp aliases do 68 | [ 69 | test: [&clean_project/1, "test"] 70 | ] 71 | end 72 | 73 | defp clean_project(_) do 74 | System.cmd("rm", ["-rf", "_build"]) 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, 3 | "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, 4 | "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, 5 | "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, 6 | "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, 7 | "credo": {:hex, :credo, "1.7.1", "6e26bbcc9e22eefbff7e43188e69924e78818e2fe6282487d0703652bc20fd62", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9871c6095a4c0381c89b6aa98bc6260a8ba6addccf7f6a53da8849c748a58a2"}, 8 | "dialyxir": {:hex, :dialyxir, "1.4.2", "764a6e8e7a354f0ba95d58418178d486065ead1f69ad89782817c296d0d746a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "516603d8067b2fd585319e4b13d3674ad4f314a5902ba8130cd97dc902ce6bbd"}, 9 | "earmark_parser": {:hex, :earmark_parser, "1.4.30", "0b938aa5b9bafd455056440cdaa2a79197ca5e693830b4a982beada840513c5f", [:mix], [], "hexpm", "3b5385c2d36b0473d0b206927b841343d25adb14f95f0110062506b300cd5a1b"}, 10 | "envar": {:hex, :envar, "1.1.0", "105bcac5a03800a1eb21e2c7e229edc687359b0cc184150ec1380db5928c115c", [:mix], [], "hexpm", "97028ab4a040a5c19e613fdf46a41cf51c6e848d99077e525b338e21d2993320"}, 11 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 12 | "ex_doc": {:hex, :ex_doc, "0.29.1", "b1c652fa5f92ee9cf15c75271168027f92039b3877094290a75abcaac82a9f77", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "b7745fa6374a36daf484e2a2012274950e084815b936b1319aeebcf7809574f6"}, 13 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 14 | "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, 15 | "httpoison": {:hex, :httpoison, "0.13.0", "bfaf44d9f133a6599886720f3937a7699466d23bb0cd7a88b6ba011f53c6f562", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "4846958172d6401c4f34ecc5c2c4607b5b0d90b8eec8f6df137ca4907942ed0f"}, 16 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 17 | "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, 18 | "jose": {:hex, :jose, "1.11.5", "3bc2d75ffa5e2c941ca93e5696b54978323191988eb8d225c2e663ddfefd515e", [:mix, :rebar3], [], "hexpm", "dcd3b215bafe02ea7c5b23dafd3eb8062a5cd8f2d904fd9caa323d37034ab384"}, 19 | "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, 20 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, 21 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 22 | "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, 23 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 24 | "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"}, 25 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 26 | "mock": {:hex, :mock, "0.3.8", "7046a306b71db2488ef54395eeb74df0a7f335a7caca4a3d3875d1fc81c884dd", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "7fa82364c97617d79bb7d15571193fc0c4fe5afd0c932cef09426b3ee6fe2022"}, 27 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, 28 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 29 | "plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"}, 30 | "plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"}, 31 | "plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"}, 32 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, 33 | "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, 34 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, 35 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, 36 | "tesla": {:hex, :tesla, "1.5.0", "7ee3616be87024a2b7231ae14474310c9b999c3abb1f4f8dbc70f86bd9678eef", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "1d0385e41fbd76af3961809088aef15dec4c2fdaab97b1c93c6484cb3695a122"}, 37 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 38 | } 39 | -------------------------------------------------------------------------------- /test/authorization_code_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AuthorizationCodeTest do 2 | use ExUnit.Case 3 | 4 | alias ClientTestHelper 5 | alias KindeClientSDK 6 | alias Plug.Conn 7 | 8 | @domain Application.compile_env(:kinde_sdk, :domain) |> String.replace("\"", "") 9 | @grant_type :authorization_code 10 | 11 | setup_all do 12 | conn = Plug.Test.conn(:get, "/") |> Plug.Test.init_test_session(%{}) 13 | {conn, client} = ClientTestHelper.initialize_valid_client(conn, @grant_type) 14 | {:ok, conn: conn, client: client} 15 | end 16 | 17 | test "initialize the client", %{conn: conn, client: client} do 18 | assert client.token_endpoint == @domain <> "/oauth2/token" 19 | refute is_nil(KindeClientSDK.get_cache_pid(conn)) 20 | end 21 | 22 | test "do login", %{conn: conn, client: client} do 23 | KindeClientSDK.save_kinde_client(conn, client) 24 | 25 | conn = KindeClientSDK.login(conn, client) 26 | 27 | refute Enum.empty?(Conn.get_resp_header(conn, "location")) 28 | end 29 | 30 | test "do login with audience", %{conn: conn, client: _} do 31 | additional_params = %{ 32 | audience: @domain <> "/api" 33 | } 34 | 35 | {conn, client} = 36 | ClientTestHelper.initialize_valid_client_add_params(conn, @grant_type, additional_params) 37 | 38 | KindeClientSDK.save_kinde_client(conn, client) 39 | 40 | conn = KindeClientSDK.login(conn, client) 41 | 42 | refute Enum.empty?(Conn.get_resp_header(conn, "location")) 43 | end 44 | 45 | test "do login with additional", %{conn: conn, client: _} do 46 | additional_params = %{ 47 | audience: @domain <> "/api" 48 | } 49 | 50 | {conn, client} = 51 | ClientTestHelper.initialize_valid_client_add_params(conn, @grant_type, additional_params) 52 | 53 | KindeClientSDK.save_kinde_client(conn, client) 54 | 55 | additional_params_more = %{ 56 | org_code: "org_123", 57 | org_name: "My Application" 58 | } 59 | 60 | conn = KindeClientSDK.login(conn, client, additional_params_more) 61 | 62 | refute Enum.empty?(Conn.get_resp_header(conn, "location")) 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/client_credentials_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ClientCredentialsTest do 2 | use ExUnit.Case 3 | 4 | alias ClientTestHelper 5 | alias KindeClientSDK 6 | 7 | @domain Application.compile_env(:kinde_sdk, :domain) |> String.replace("\"", "") 8 | @grant_type :client_credentials 9 | 10 | setup_all do 11 | conn = Plug.Test.conn(:get, "/") |> Plug.Test.init_test_session(%{}) 12 | {conn, client} = ClientTestHelper.initialize_valid_client(conn, @grant_type) 13 | {:ok, conn: conn, client: client} 14 | end 15 | 16 | test "initialize the client", %{conn: conn, client: client} do 17 | assert client.token_endpoint == @domain <> "/oauth2/token" 18 | refute is_nil(KindeClientSDK.get_cache_pid(conn)) 19 | end 20 | 21 | test "get access token", %{conn: conn, client: client} do 22 | KindeClientSDK.save_kinde_client(conn, client) 23 | 24 | KindeClientSDK.login(conn, client) 25 | 26 | data = KindeClientSDK.get_all_data(conn) 27 | 28 | refute is_nil(data.access_token) 29 | end 30 | 31 | test "login with audience", %{conn: conn, client: client} do 32 | additional_params = %{ 33 | audience: @domain <> "/api" 34 | } 35 | 36 | KindeClientSDK.save_kinde_client(conn, client) 37 | 38 | assert KindeClientSDK.login(conn, client, additional_params) 39 | end 40 | 41 | test "login with org_code", %{conn: conn, client: client} do 42 | additional_params = %{ 43 | org_code: "org_123", 44 | org_name: "My Application" 45 | } 46 | 47 | KindeClientSDK.save_kinde_client(conn, client) 48 | 49 | KindeClientSDK.login(conn, client, additional_params) 50 | 51 | data = KindeClientSDK.get_all_data(conn) 52 | 53 | refute is_nil(data.access_token) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/client_test_helper.exs: -------------------------------------------------------------------------------- 1 | defmodule ClientTestHelper do 2 | alias KindeClientSDK 3 | alias KindeSDK.SDK.Utils 4 | 5 | @redirect_url Application.compile_env(:kinde_sdk, :redirect_url) |> String.replace("\"", "") 6 | @pkce_callback_url Application.compile_env(:kinde_sdk, :pkce_callback_url) 7 | |> String.replace("\"", "") 8 | @frontend_client_id Application.compile_env(:kinde_sdk, :frontend_client_id) 9 | |> String.replace("\"", "") 10 | @client_id Application.compile_env(:kinde_sdk, :backend_client_id) |> String.replace("\"", "") 11 | @client_secret Application.compile_env(:kinde_sdk, :client_secret) |> String.replace("\"", "") 12 | @logout_redirect_url Application.compile_env(:kinde_sdk, :logout_redirect_url) 13 | |> String.replace("\"", "") 14 | @valid_domain Application.compile_env(:kinde_sdk, :domain) |> String.replace("\"", "") 15 | @invalid_domain "test.c" 16 | 17 | def initialize_valid_client(conn, grant_type) do 18 | KindeClientSDK.init( 19 | conn, 20 | @valid_domain, 21 | @redirect_url, 22 | @client_id, 23 | @client_secret, 24 | grant_type, 25 | @logout_redirect_url 26 | ) 27 | end 28 | 29 | def initialize_invalid_client(conn, grant_type) do 30 | KindeClientSDK.init( 31 | conn, 32 | @invalid_domain, 33 | @redirect_url, 34 | @client_id, 35 | @client_secret, 36 | grant_type, 37 | @logout_redirect_url 38 | ) 39 | end 40 | 41 | def initialize_invalid_redirect_uri(conn, grant_type) do 42 | KindeClientSDK.init( 43 | conn, 44 | @valid_domain, 45 | "", 46 | @client_id, 47 | @client_secret, 48 | grant_type, 49 | @logout_redirect_url 50 | ) 51 | end 52 | 53 | def initialize_valid_client_add_params(conn, grant_type, additional_params) do 54 | KindeClientSDK.init( 55 | conn, 56 | @valid_domain, 57 | @redirect_url, 58 | @client_id, 59 | @client_secret, 60 | grant_type, 61 | @logout_redirect_url, 62 | "openid profile email offline", 63 | additional_params 64 | ) 65 | end 66 | 67 | def init_valid_pkce_client(conn, grant_type) do 68 | KindeClientSDK.init( 69 | conn, 70 | @valid_domain, 71 | @pkce_callback_url, 72 | @frontend_client_id, 73 | @client_secret, 74 | grant_type, 75 | @logout_redirect_url 76 | ) 77 | end 78 | 79 | def mock_token(conn, pid) do 80 | token = %{ 81 | "id_token" => 82 | "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJLaW5kZSBFbGl4aXIgVGVzdCIsImlhdCI6MTY4MDYwODY3NSwiZXhwIjoxNzEyMTQ0Njc1LCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJ0ZXN0QGtpbmRlLmNvbSIsImdpdmVuX25hbWUiOiJKb2huIiwiZmFtaWx5X25hbWUiOiJEb2UiLCJlbWFpbCI6InVzZXJAa2luZGUuY29tIiwib3JnX2NvZGUiOiI3NjU1NDYifQ.7oLWhK6MhMdOWhExd7XTKUH-FvcfUBtBZQjvyKWmSyE", 83 | "access_token" => nil, 84 | "expires_in" => 99_987 85 | } 86 | 87 | expires_in = if is_nil(token["expires_in"]), do: 0, else: token["expires_in"] 88 | 89 | GenServer.cast(pid, {:add_kinde_data, {:kinde_login_time_stamp, DateTime.utc_now()}}) 90 | GenServer.cast(pid, {:add_kinde_data, {:kinde_access_token, token["access_token"]}}) 91 | GenServer.cast(pid, {:add_kinde_data, {:kinde_id_token, token["id_token"]}}) 92 | GenServer.cast(pid, {:add_kinde_data, {:kinde_expires_in, expires_in}}) 93 | 94 | payload = Utils.parse_jwt(token["id_token"]) 95 | 96 | if is_nil(payload) do 97 | GenServer.cast(pid, {:add_kinde_data, {:kinde_user, nil}}) 98 | else 99 | user = %{ 100 | id: payload["sub"], 101 | given_name: payload["given_name"], 102 | family_name: payload["family_name"], 103 | email: payload["email"] 104 | } 105 | 106 | GenServer.cast(pid, {:add_kinde_data, {:kinde_user, user}}) 107 | end 108 | 109 | conn 110 | end 111 | 112 | def mock_feature_flags(conn, pid) do 113 | token = %{ 114 | "access_token" => 115 | "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdhOmYzOjZiOmE3OjVjOjI1OmE3OmY4OmMzOjA1OjYzOmNiOjRiOmYwOjMzOjhiIiwidHlwIjoiSldUIn0.eyJhdWQiOltdLCJhenAiOiIyZTQxZTllNzk4MDA0MTc3YjFiNDRhYTFlMWFkM2Q2MiIsImV4cCI6MTY4NDM5NjYzMCwiZmVhdHVyZV9mbGFncyI6eyJjb3VudGVyIjp7InQiOiJpIiwidiI6NTV9LCJpc19kYXJrX21vZGUiOnsidCI6ImIiLCJ2IjpmYWxzZX0sInRoZW1lIjp7InQiOiJzIiwidiI6ImdyYXlzY2FsZSJ9fSwiaWF0IjoxNjg0Mzk2NTY5LCJpc3MiOiJodHRwczovL2VsaXhpcnNkazIua2luZGUuY29tIiwianRpIjoiYzljOTM0YTctOGRmOC00OWFjLWFkZDctODRiNDMwODg3YWYyIiwib3JnX2NvZGUiOiJvcmdfOWRjMzNmMWQ2NDQiLCJwZXJtaXNzaW9ucyI6W10sInNjcCI6WyJvcGVuaWQiLCJwcm9maWxlIiwiZW1haWwiLCJvZmZsaW5lIl0sInN1YiI6ImtwOmEzNjgzMTJlM2Q4ODRlMTU4YmU0NzVjNjAyNWJiYmFkIn0.vM0s0KKp8Y_KcXgBtuIWlskcyiSyhBRBNV-7hOWUarUr9wu61P1L-pjYswdRj_0HKH7ZxaOydSl1Mq-6tf7R8uWryR-kLVVFRpCvCDP7y3CLsGkBlLsnxazwWuBmja0619oBTqjba7QAVE3rxlIuUYdNLjJrXbo0V0OAwErzlB8gGEJ5s2opvpWKtKjO027SNDEGSHbWJ3SGvMRYtZSRA9ku3Tcso3eqLH2cFT7tS0aYRWyPqvZe3k_st4T0qoRXGYgQI_XMTgw26ar2yvXp7Shvl3ib6oUndOr_ZQ5yT3orvvl8wyadgct3X-i8c379pn8kTvZ3kTN2r-jYLQVAEQ" 116 | } 117 | 118 | GenServer.cast(pid, {:add_kinde_data, {:kinde_access_token, token["access_token"]}}) 119 | 120 | conn 121 | end 122 | 123 | def mock_picture_url(conn, pid) do 124 | token = %{ 125 | "id_token" => 126 | "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdhOmYzOjZiOmE3OjVjOjI1OmE3OmY4OmMzOjA1OjYzOmNiOjRiOmYwOjMzOjhiIiwidHlwIjoiSldUIn0.eyJhdF9oYXNoIjoiXzBBV1kteF9SZnd1NHduMVdhMV9NQSIsImF1ZCI6WyJodHRwczovL2VsaXhpcnNkazIua2luZGUuY29tIiwiMmU0MWU5ZTc5ODAwNDE3N2IxYjQ0YWExZTFhZDNkNjIiXSwiYXV0aF90aW1lIjoxNjg1NDYyNjU0LCJhenAiOiIyZTQxZTllNzk4MDA0MTc3YjFiNDRhYTFlMWFkM2Q2MiIsImVtYWlsIjoiYWhtZWQuaXNtYWlsQGludm96b25lLmNvbSIsImV4cCI6MTY4NTQ2MjcxNCwiZmFtaWx5X25hbWUiOiJBaG1lZCBJc21haWwiLCJnaXZlbl9uYW1lIjoiTXVoYW1tYWQiLCJpYXQiOjE2ODU0NjI2NTQsImlzcyI6Imh0dHBzOi8vZWxpeGlyc2RrMi5raW5kZS5jb20iLCJqdGkiOiI3N2MxMjUxYi1hYmY0LTRjNTctOGUxOS1mYzQ2NDc4OTVjNzYiLCJuYW1lIjoiTXVoYW1tYWQgQWhtZWQgSXNtYWlsIiwib3JnX2NvZGVzIjpbIm9yZ185ZGMzM2YxZDY0NCJdLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EvQUFjSFR0ZndiOHlHOHhpOFozM0xVQ1htbngtNDBuRXlQVjYxTkFsVHJEc2Q9czk2LWMiLCJzdWIiOiJrcDphMzY4MzEyZTNkODg0ZTE1OGJlNDc1YzYwMjViYmJhZCIsInVwZGF0ZWRfYXQiOjEuNjg1NDYyNjUzZSswOX0.mPc9D4xXppJLe_JqPODzBzZxULYkgVEG8uUAWSRw0GFOj8zq3rpCiaYClubL_l2mlojgFEWJnwvX1Jjei2jjKDjZ4WfwAYywIDnodgZ2F4AUSvjY1bR7jT6ZqjSvYU6_AsvF3Dp2VSmY5R0xudxQrC1tm68jFLBrrD8RMcbJg_BJ8hRWEMHbg58BBBOdsuOuI1wiqIozlQWHe74B2RaZNuNCHjfQ81e71IYU8z9ibbDF6-xxyhMKGnDH1-b3mlW34i4RO7dTVrXSjz9L0wubTD2ECTMCQ8ym9UUtO6-Sq3wZZl_ztudaRTdiwBiDeJGHgvR9AStmfVhf6iFmrLUTUw" 127 | } 128 | 129 | payload = Utils.parse_jwt(token["id_token"]) 130 | 131 | if is_nil(payload) do 132 | GenServer.cast(pid, {:add_kinde_data, {:kinde_user, nil}}) 133 | else 134 | user = %{ 135 | id: payload["sub"], 136 | given_name: payload["given_name"], 137 | family_name: payload["family_name"], 138 | email: payload["email"], 139 | picture: payload["picture"] 140 | } 141 | 142 | GenServer.cast(pid, {:add_kinde_data, {:kinde_user, user}}) 143 | end 144 | 145 | conn 146 | end 147 | 148 | def mock_pkce_token(conn, pid) do 149 | token = %{ 150 | "access_token" => 151 | "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdhOmYzOjZiOmE3OjVjOjI1OmE3OmY4OmMzOjA1OjYzOmNiOjRiOmYwOjMzOjhiIiwidHlwIjoiSldUIn0.eyJhdWQiOltdLCJhenAiOiIyZTQxZTllNzk4MDA0MTc3YjFiNDRhYTFlMWFkM2Q2MiIsImV4cCI6MTY4NTUyMjg0MiwiZmVhdHVyZV9mbGFncyI6eyJjb3VudGVyIjp7InQiOiJpIiwidiI6NTV9LCJpc19kYXJrX21vZGUiOnsidCI6ImIiLCJ2IjpmYWxzZX0sInRoZW1lIjp7InQiOiJzIiwidiI6ImdyYXlzY2FsZSJ9fSwiaWF0IjoxNjg1NTIyNzgyLCJpc3MiOiJodHRwczovL2VsaXhpcnNkazIua2luZGUuY29tIiwianRpIjoiMzFmNzBlMzQtODRlYy00ODE5LWI0ZTEtMWJlYWNlMWQwOTVjIiwib3JnX2NvZGUiOiJvcmdfOWRjMzNmMWQ2NDQiLCJwZXJtaXNzaW9ucyI6W10sInNjcCI6WyJvcGVuaWQiLCJwcm9maWxlIiwiZW1haWwiLCJvZmZsaW5lIl0sInN1YiI6ImtwOmEzNjgzMTJlM2Q4ODRlMTU4YmU0NzVjNjAyNWJiYmFkIn0.vDZ7LnzlYhYKrJhpk3eb_435A4ecyiEtR3S2D0TTDMKZx6JP-i8jKfimwzjCQmd6L7KRohJjj8ClNK2pdykEu-HRKiPZLOzni74tNMzIrjaQwvrmz4qEf2OEUE3IrLmHgZ2phIqJmqBN8albfdivm2RYesRt68TKakkqs-I8vU9eyAffRQH7UkKmzmAbhC69N4Y3auJefNFtqRlbUZ0-gAyBCeBLErFmgcoWyTpUWnKPlps7hCQNqA-q3JkXnsKX-WPFNm5LIF2qUjCAjMJLAKQW6Xc66LfsTWiuwMSF_NlUSK56tfv9089QGV_dZ7EODAzDM2P8hxnxvpEv6WSRbw", 152 | "expires_in" => 59, 153 | "id_token" => 154 | "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdhOmYzOjZiOmE3OjVjOjI1OmE3OmY4OmMzOjA1OjYzOmNiOjRiOmYwOjMzOjhiIiwidHlwIjoiSldUIn0.eyJhdF9oYXNoIjoidTQtLVJEMjAyWTM5c0lURXN1SENNdyIsImF1ZCI6WyJodHRwczovL2VsaXhpcnNkazIua2luZGUuY29tIiwiMmU0MWU5ZTc5ODAwNDE3N2IxYjQ0YWExZTFhZDNkNjIiXSwiYXV0aF90aW1lIjoxNjg1NTIyNzgyLCJhenAiOiIyZTQxZTllNzk4MDA0MTc3YjFiNDRhYTFlMWFkM2Q2MiIsImVtYWlsIjoiYWhtZWQuaXNtYWlsQGludm96b25lLmNvbSIsImV4cCI6MTY4NTUyMjg0MiwiZmFtaWx5X25hbWUiOiJBaG1lZCBJc21haWwiLCJnaXZlbl9uYW1lIjoiTXVoYW1tYWQiLCJpYXQiOjE2ODU1MjI3ODIsImlzcyI6Imh0dHBzOi8vZWxpeGlyc2RrMi5raW5kZS5jb20iLCJqdGkiOiI2MDI4OGRkYi1jMjRiLTRmODYtYjUwZS01ZWZiNDAzOGMxMjciLCJuYW1lIjoiTXVoYW1tYWQgQWhtZWQgSXNtYWlsIiwib3JnX2NvZGVzIjpbIm9yZ185ZGMzM2YxZDY0NCJdLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EvQUFjSFR0ZndiOHlHOHhpOFozM0xVQ1htbngtNDBuRXlQVjYxTkFsVHJEc2Q9czk2LWMiLCJzdWIiOiJrcDphMzY4MzEyZTNkODg0ZTE1OGJlNDc1YzYwMjViYmJhZCIsInVwZGF0ZWRfYXQiOjEuNjg1NTIyNzIxZSswOX0.B6FUOesnmnWa1IUM8e36zPMPLu-9lINmtUqoapte2gnd1_0BDVfXJVcfaKu7ekykc76LdF8l8_SOYGzh54xkJuMffIP6l2I5yEi35pXXuS10gP1AO6alCgFwUnBfNVP2jX1Np4IDPocxUojJxcOd_NGxaEOogaVnqWZobfkZdb1hNkUg-FYMYQYVtmnLj5sVCouTdLCyqKT_HlKfWgYxcWauoDzIcHnE1io8JGRNmPu-Nf_o8Czm10pgXVZNdVVLNpzRhAVmSieZZNDZi5iVGEwghcFLroujpvHDJESfiIFnbGbR50jTleSjoVZJjgvJN3JCt0m_CQqKcCXPoqrAPw", 155 | "refresh_token" => 156 | "h-GHFrU_9dRPBJxEIkJjYQBoy-pI0y52fuiFbfGndX4.auX9g-Pp02sM1YBn-nkmCWrynGSy_AvY7RmipN5uUwI", 157 | "scope" => "openid profile email offline", 158 | "token_type" => "bearer" 159 | } 160 | 161 | GenServer.cast(pid, {:add_kinde_data, {:kinde_access_token, token["access_token"]}}) 162 | GenServer.cast(pid, {:add_kinde_data, {:kinde_id_token, token["id_token"]}}) 163 | GenServer.cast(pid, {:add_kinde_data, {:kinde_expires_in, token["expires_in"]}}) 164 | GenServer.cast(pid, {:add_kinde_data, {:kinde_refresh_token, token["refresh_token"]}}) 165 | 166 | conn 167 | end 168 | 169 | def mock_result_for_refresh_token(conn, pid) do 170 | token = %{ 171 | "access_token" => 172 | "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdhOmYzOjZiOmE3OjVjOjI1OmE3OmY4OmMzOjA1OjYzOmNiOjRiOmYwOjMzOjhiIiwidHlwIjoiSldUIn0.eyJhdWQiOltdLCJhenAiOiIyZTQxZTllNzk4MDA0MTc3YjFiNDRhYTFlMWFkM2Q2MiIsImV4cCI6MTY4NTUyNjY4MywiZmVhdHVyZV9mbGFncyI6eyJjb3VudGVyIjp7InQiOiJpIiwidiI6NTV9LCJpc19kYXJrX21vZGUiOnsidCI6ImIiLCJ2IjpmYWxzZX0sInRoZW1lIjp7InQiOiJzIiwidiI6ImdyYXlzY2FsZSJ9fSwiaWF0IjoxNjg1NTI2NjIyLCJpc3MiOiJodHRwczovL2VsaXhpcnNkazIua2luZGUuY29tIiwianRpIjoiOGQyMTgxYjgtNjcxZC00ZTdmLWFkNDgtYTIxYmJhNjlhM2ZhIiwib3JnX2NvZGUiOiJvcmdfOWRjMzNmMWQ2NDQiLCJwZXJtaXNzaW9ucyI6W10sInNjcCI6WyJvcGVuaWQiLCJwcm9maWxlIiwiZW1haWwiLCJvZmZsaW5lIl0sInN1YiI6ImtwOmEzNjgzMTJlM2Q4ODRlMTU4YmU0NzVjNjAyNWJiYmFkIn0.jarRvDXeeX26J_hdxDjhGgUgbOJYXIOgk48EQg4zstISGGP4Dev2b91vM4cQAW2X8RSnXeVcJJ7u1_6Yz19sw14aPrJlvuDcVpuADiPFE4phkAvRXTPvr2iJpC0OVz6ZHqKBsMciu3bCi61uojyAKvjD6pn7rYiHWmxoFeZnmPxLK1cGBrMfBzX0MXXykMMF78NEpthempjhuM3kMPlyao7H1LCO4Os6mKPDzAliyv4t5T465wLIC_xTr1Kdp456eLU-FyJLD2bWWtKOzMSf2pOXt99UNtrJl7Pcyi7C7t1Z_iyPvLhU6YnPGe4UICKcuVYC3--TbZfGkg5kr2JKNw", 173 | "expires_in" => 60, 174 | "id_token" => 175 | "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdhOmYzOjZiOmE3OjVjOjI1OmE3OmY4OmMzOjA1OjYzOmNiOjRiOmYwOjMzOjhiIiwidHlwIjoiSldUIn0.eyJhdF9oYXNoIjoiQVpGSHdSbkpyN1dZWm1mRXJpd3ZBZyIsImF1ZCI6WyJodHRwczovL2VsaXhpcnNkazIua2luZGUuY29tIiwiMmU0MWU5ZTc5ODAwNDE3N2IxYjQ0YWExZTFhZDNkNjIiXSwiYXV0aF90aW1lIjoxNjg1NTI2NjIyLCJhenAiOiIyZTQxZTllNzk4MDA0MTc3YjFiNDRhYTFlMWFkM2Q2MiIsImVtYWlsIjoiYWhtZWQuaXNtYWlsQGludm96b25lLmNvbSIsImV4cCI6MTY4NTUyNjY4MiwiZmFtaWx5X25hbWUiOiJBaG1lZCBJc21haWwiLCJnaXZlbl9uYW1lIjoiTXVoYW1tYWQiLCJpYXQiOjE2ODU1MjY2MjIsImlzcyI6Imh0dHBzOi8vZWxpeGlyc2RrMi5raW5kZS5jb20iLCJqdGkiOiIyZjVmZmY3NC1jYjU3LTRlNWQtOWU2My0zZDVjZTMxNDE4YzYiLCJuYW1lIjoiTXVoYW1tYWQgQWhtZWQgSXNtYWlsIiwib3JnX2NvZGVzIjpbIm9yZ185ZGMzM2YxZDY0NCJdLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EvQUFjSFR0ZndiOHlHOHhpOFozM0xVQ1htbngtNDBuRXlQVjYxTkFsVHJEc2Q9czk2LWMiLCJzdWIiOiJrcDphMzY4MzEyZTNkODg0ZTE1OGJlNDc1YzYwMjViYmJhZCIsInVwZGF0ZWRfYXQiOjEuNjg1NTIyNzIxZSswOX0.pQzIPfbGbS8pxU55aHdqIMSsg8S8lTQ7cCPnrjnIIAIwtuG22MxxFjryMzX0zcW8HK2zmBuKTvrgx6-dI34wPT2uDyvvP3vpXS76h5Yz4-06Bn4y11DfnOGEk1CNxhv-8a2F9khCSWrM8Pi9WOBd3R1QHY4neDdYK-SF_tJ2yoouFTDOe8XzYynt7UBesBqzWzibpoDIymU_l0BMkz-abdRhtdgctbu6R42GVEEedH3uvQuIXwfyUOwfXpdAl5QuA0Hp6hWogFkrCGQTDF6k8kNimZmDXTTyDoAcqy7ubQrXDcq6O5wHLz5kLJ3UxPuOztlrmghSTR1h3UZAFJXEVw", 176 | "refresh_token" => 177 | "TrpKSDbbvSjIlg204MItcvDZXNEXzhh_TifwLvscb1s.xpgl4r4GQatmaKn-GFa9tm152Oyf8U4a0c2kagFFEr0", 178 | "scope" => "openid profile email offline", 179 | "token_type" => "bearer" 180 | } 181 | 182 | GenServer.cast(pid, {:add_kinde_data, {:kinde_access_token, token["access_token"]}}) 183 | GenServer.cast(pid, {:add_kinde_data, {:kinde_id_token, token["id_token"]}}) 184 | GenServer.cast(pid, {:add_kinde_data, {:kinde_expires_in, token["expires_in"]}}) 185 | GenServer.cast(pid, {:add_kinde_data, {:kinde_refresh_token, token["refresh_token"]}}) 186 | 187 | conn 188 | end 189 | end 190 | -------------------------------------------------------------------------------- /test/kinde_client_sdk_test.exs: -------------------------------------------------------------------------------- 1 | defmodule KindeClientSDKTest do 2 | use ExUnit.Case 3 | 4 | alias ClientTestHelper 5 | alias KindeClientSDK 6 | alias Plug.Conn 7 | 8 | @domain Application.compile_env(:kinde_sdk, :domain) |> String.replace("\"", "") 9 | @grant_type :client_credentials 10 | 11 | setup_all do 12 | {:ok, conn: Plug.Test.conn(:get, "/") |> Plug.Test.init_test_session(%{})} 13 | end 14 | 15 | test "invalid domain", %{conn: conn} do 16 | assert catch_throw(ClientTestHelper.initialize_invalid_client(conn, @grant_type)) == 17 | "Please provide valid domain" 18 | end 19 | 20 | test "empty redirect_uri", %{conn: conn} do 21 | assert catch_throw(ClientTestHelper.initialize_invalid_redirect_uri(conn, @grant_type)) == 22 | "Please provide valid redirect_uri" 23 | end 24 | 25 | test "cache pid", %{conn: conn} do 26 | {conn, _client} = ClientTestHelper.initialize_valid_client(conn, @grant_type) 27 | 28 | refute is_nil(Conn.get_session(conn, :kinde_cache_pid)) 29 | end 30 | 31 | test "init", %{conn: conn} do 32 | {_conn, client} = ClientTestHelper.initialize_valid_client(conn, @grant_type) 33 | 34 | assert client.token_endpoint == @domain <> "/oauth2/token" 35 | end 36 | 37 | test "invalid grant type login", %{conn: conn} do 38 | {conn, client} = ClientTestHelper.initialize_valid_client(conn, :invalid_grant_type) 39 | 40 | assert catch_throw(KindeClientSDK.login(conn, client)) == "Please provide correct grant_type" 41 | end 42 | 43 | test "get valid grant type" do 44 | assert KindeClientSDK.get_grant_type(:authorization_code_flow_pkce) == :authorization_code 45 | end 46 | 47 | test "get invalid grant type" do 48 | assert catch_throw(KindeClientSDK.get_grant_type(:invalid)) == 49 | "Please provide correct grant_type" 50 | end 51 | 52 | test "valid audience", %{conn: conn} do 53 | additional_params = %{ 54 | audience: @domain <> "/api" 55 | } 56 | 57 | {_conn, client} = 58 | ClientTestHelper.initialize_valid_client_add_params(conn, @grant_type, additional_params) 59 | 60 | assert client.additional_params == additional_params 61 | end 62 | 63 | test "invalid audience", %{conn: conn} do 64 | additional_params = %{ 65 | audience: 12_345 66 | } 67 | 68 | assert catch_throw( 69 | ClientTestHelper.initialize_valid_client_add_params( 70 | conn, 71 | @grant_type, 72 | additional_params 73 | ) 74 | ) == "Please supply a valid audience. Expected: string" 75 | end 76 | 77 | test "is authenticated", %{conn: conn} do 78 | {conn, _client} = ClientTestHelper.initialize_valid_client(conn, @grant_type) 79 | 80 | refute KindeClientSDK.authenticated?(conn) 81 | end 82 | 83 | test "login invalid org code", %{conn: conn} do 84 | additional_params = %{ 85 | org_code: 12_345, 86 | org_name: "Test App" 87 | } 88 | 89 | {conn, client} = ClientTestHelper.initialize_valid_client(conn, @grant_type) 90 | 91 | assert catch_throw(KindeClientSDK.login(conn, client, additional_params)) == 92 | "Please supply a valid org_code. Expected: string" 93 | end 94 | 95 | test "login invalid org name", %{conn: conn} do 96 | additional_params = %{ 97 | org_code: "12345", 98 | org_name: 123 99 | } 100 | 101 | {conn, client} = ClientTestHelper.initialize_valid_client(conn, @grant_type) 102 | 103 | assert catch_throw(KindeClientSDK.login(conn, client, additional_params)) == 104 | "Please supply a valid org_name. Expected: string" 105 | end 106 | 107 | test "login invalid additional org code", %{conn: conn} do 108 | additional_params = %{ 109 | org_code: "12345", 110 | org_name_test: "Test App" 111 | } 112 | 113 | {conn, client} = ClientTestHelper.initialize_valid_client(conn, @grant_type) 114 | 115 | assert catch_throw(KindeClientSDK.login(conn, client, additional_params)) == 116 | "Please provide correct additional, org_name_test" 117 | end 118 | 119 | test "login invalid additional org name", %{conn: conn} do 120 | additional_params = %{ 121 | org_code_test: "12345", 122 | org_name: "123" 123 | } 124 | 125 | {conn, client} = ClientTestHelper.initialize_valid_client(conn, @grant_type) 126 | 127 | assert catch_throw(KindeClientSDK.login(conn, client, additional_params)) == 128 | "Please provide correct additional, org_code_test" 129 | end 130 | 131 | test "get user for unauthenticated client", %{conn: conn} do 132 | {conn, _} = ClientTestHelper.initialize_valid_client(conn, :authorization_code) 133 | 134 | assert is_nil(KindeClientSDK.get_user_detail(conn)) 135 | end 136 | 137 | test "get user for authenticated client", %{conn: conn} do 138 | {conn, client} = ClientTestHelper.initialize_valid_client(conn, :authorization_code) 139 | 140 | conn = ClientTestHelper.mock_token(conn, client.cache_pid) 141 | 142 | assert KindeClientSDK.get_user_detail(conn) == %{ 143 | email: "user@kinde.com", 144 | family_name: "Doe", 145 | given_name: "John", 146 | id: "test@kinde.com" 147 | } 148 | end 149 | 150 | test "get user with picture_url", %{conn: conn} do 151 | {conn, client} = ClientTestHelper.initialize_valid_client(conn, :authorization_code) 152 | 153 | conn = ClientTestHelper.mock_picture_url(conn, client.cache_pid) 154 | 155 | user_details = KindeClientSDK.get_user_detail(conn) 156 | 157 | assert user_details.picture == 158 | "https://lh3.googleusercontent.com/a/AAcHTtfwb8yG8xi8Z33LUCXmnx-40nEyPV61NAlTrDsd=s96-c" 159 | end 160 | 161 | test "get user for authenticated client_credentials grant", %{conn: conn} do 162 | {conn, client} = ClientTestHelper.initialize_valid_client(conn, @grant_type) 163 | 164 | KindeClientSDK.save_kinde_client(conn, client) 165 | 166 | conn = KindeClientSDK.login(conn, client) 167 | 168 | assert is_nil(KindeClientSDK.get_user_detail(conn)) 169 | end 170 | 171 | test "permissions for unauthenticated client", %{conn: conn} do 172 | {conn, _} = ClientTestHelper.initialize_valid_client(conn, :authorization_code) 173 | 174 | assert catch_throw(KindeClientSDK.get_permissions(conn)) == 175 | "Request is missing required authentication credential" 176 | end 177 | 178 | test "permissions for authenticated client", %{conn: conn} do 179 | {conn, client} = ClientTestHelper.initialize_valid_client(conn, :authorization_code) 180 | 181 | conn = ClientTestHelper.mock_token(conn, client.cache_pid) 182 | 183 | assert KindeClientSDK.get_permissions(conn, :id_token) == 184 | %{org_code: "765546", permissions: nil} 185 | end 186 | 187 | test "permissions for authenticated client_credential client", %{conn: conn} do 188 | {conn, client} = ClientTestHelper.initialize_valid_client(conn, @grant_type) 189 | 190 | KindeClientSDK.save_kinde_client(conn, client) 191 | 192 | conn = KindeClientSDK.login(conn, client) 193 | 194 | assert KindeClientSDK.get_permissions(conn) == %{org_code: nil, permissions: nil} 195 | end 196 | 197 | test "save client", %{conn: conn} do 198 | {conn, client} = ClientTestHelper.initialize_valid_client(conn, :authorization_code) 199 | client = KindeClientSDK.save_kinde_client(conn, client) 200 | 201 | assert client == :ok 202 | end 203 | 204 | test "get client", %{conn: conn} do 205 | {conn, client} = ClientTestHelper.initialize_valid_client(conn, :authorization_code) 206 | KindeClientSDK.save_kinde_client(conn, client) 207 | 208 | get_client = KindeClientSDK.get_kinde_client(conn) 209 | 210 | assert client.domain == get_client.domain 211 | end 212 | 213 | test "get cache pid", %{conn: conn} do 214 | assert is_nil(KindeClientSDK.get_cache_pid(conn)) 215 | 216 | {conn, _} = ClientTestHelper.initialize_valid_client(conn, :authorization_code) 217 | assert !is_nil(KindeClientSDK.get_cache_pid(conn)) 218 | end 219 | 220 | test "get all data", %{conn: conn} do 221 | {conn, _} = ClientTestHelper.initialize_valid_client(conn, :authorization_code) 222 | data = KindeClientSDK.get_all_data(conn) 223 | 224 | assert is_nil(data.token) 225 | end 226 | 227 | describe "test get_claim/3 action" do 228 | test "return claim from access-token", %{conn: conn} do 229 | {conn, client} = ClientTestHelper.initialize_valid_client(conn, :client_credentials) 230 | conn = ClientTestHelper.mock_token(conn, client.cache_pid) 231 | conn = KindeClientSDK.login(conn, client) 232 | 233 | assert KindeClientSDK.get_claim(conn, "iss") == %{ 234 | name: "iss", 235 | value: Application.get_env(:kinde_sdk, :domain) |> String.replace("\"", "") 236 | } 237 | end 238 | 239 | test "throws missing-required-auth-cred error when not called with proper creds", %{ 240 | conn: conn 241 | } do 242 | {conn, client} = 243 | ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 244 | 245 | conn = ClientTestHelper.mock_token(conn, client.cache_pid) 246 | 247 | conn = KindeClientSDK.login(conn, client) 248 | 249 | assert catch_throw(KindeClientSDK.get_permissions(conn)) == 250 | "Request is missing required authentication credential" 251 | end 252 | end 253 | 254 | describe "get_flag/2 action" do 255 | test "returns detailed map for any certain code", %{conn: conn} do 256 | {conn, client} = 257 | ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 258 | 259 | conn = ClientTestHelper.mock_feature_flags(conn, client.cache_pid) 260 | 261 | assert KindeClientSDK.get_flag(conn, "theme") == %{ 262 | "code" => "theme", 263 | "is_default" => false, 264 | "type" => "string", 265 | "value" => "grayscale" 266 | } 267 | end 268 | 269 | test "returns error-message when unknown-code is passed", %{conn: conn} do 270 | {conn, client} = 271 | ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 272 | 273 | conn = ClientTestHelper.mock_feature_flags(conn, client.cache_pid) 274 | 275 | assert KindeClientSDK.get_flag(conn, "unknown_flag_code") == 276 | "This flag does not exist, and no default value provided" 277 | 278 | assert KindeClientSDK.get_flag(conn, "another_invalid_code") == 279 | "This flag does not exist, and no default value provided" 280 | end 281 | end 282 | 283 | describe "get_flag/3 action" do 284 | test "returns detailed map for any certain code, despite of any default-value provided", %{ 285 | conn: conn 286 | } do 287 | {conn, client} = 288 | ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 289 | 290 | conn = ClientTestHelper.mock_feature_flags(conn, client.cache_pid) 291 | 292 | assert KindeClientSDK.get_flag(conn, "theme", "pink") == %{ 293 | "code" => "theme", 294 | "is_default" => false, 295 | "type" => "string", 296 | "value" => "grayscale" 297 | } 298 | end 299 | 300 | test "returns customized-map for any certain code (which doesn't exists), but default-value provided", 301 | %{conn: conn} do 302 | {conn, client} = 303 | ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 304 | 305 | conn = ClientTestHelper.mock_feature_flags(conn, client.cache_pid) 306 | 307 | assert KindeClientSDK.get_flag(conn, "unknown_flag_code", "pink") == %{ 308 | "code" => "unknown_flag_code", 309 | "is_default" => true, 310 | "value" => "pink" 311 | } 312 | end 313 | end 314 | 315 | describe "get_flag/4 action" do 316 | test "returns detailed map for any certain code, when flag-type matches the type of code", %{ 317 | conn: conn 318 | } do 319 | {conn, client} = 320 | ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 321 | 322 | conn = ClientTestHelper.mock_feature_flags(conn, client.cache_pid) 323 | 324 | assert KindeClientSDK.get_flag(conn, "theme", "pink", "s") == %{ 325 | "code" => "theme", 326 | "is_default" => false, 327 | "type" => "string", 328 | "value" => "grayscale" 329 | } 330 | end 331 | 332 | test "returns error-message when types are mis-matched", %{conn: conn} do 333 | {conn, client} = 334 | ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 335 | 336 | conn = ClientTestHelper.mock_feature_flags(conn, client.cache_pid) 337 | 338 | assert KindeClientSDK.get_flag(conn, "theme", "pink", "i") == 339 | "The flag type was provided as integer, but it is string" 340 | 341 | assert KindeClientSDK.get_flag(conn, "counter", 34, "s") == 342 | "The flag type was provided as string, but it is integer" 343 | end 344 | end 345 | 346 | describe "get_boolean_flag/2 action" do 347 | test "returns true/false, when boolean-flag is fetched", %{conn: conn} do 348 | {conn, client} = 349 | ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 350 | 351 | conn = ClientTestHelper.mock_feature_flags(conn, client.cache_pid) 352 | assert KindeClientSDK.get_boolean_flag(conn, "is_dark_mode") == false 353 | end 354 | 355 | test "returns error-message, if you try to fetch non-boolean flag from it", %{conn: conn} do 356 | {conn, client} = 357 | ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 358 | 359 | conn = ClientTestHelper.mock_feature_flags(conn, client.cache_pid) 360 | 361 | assert KindeClientSDK.get_boolean_flag(conn, "theme") == 362 | "Error - Flag theme is of type string not boolean" 363 | end 364 | 365 | test "returns error-message, if flag is invalid, and doesn't exists", %{conn: conn} do 366 | {conn, client} = 367 | ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 368 | 369 | conn = ClientTestHelper.mock_feature_flags(conn, client.cache_pid) 370 | 371 | assert KindeClientSDK.get_boolean_flag(conn, "unknown_flag") == 372 | "Error - flag does not exist and no default provided" 373 | end 374 | end 375 | 376 | describe "get_boolean_flag/3 action" do 377 | test "returns true/false, when boolean-flag is fetched, despite of what default-value is being passed", 378 | %{conn: conn} do 379 | {conn, client} = 380 | ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 381 | 382 | conn = ClientTestHelper.mock_feature_flags(conn, client.cache_pid) 383 | assert KindeClientSDK.get_boolean_flag(conn, "is_dark_mode", false) == false 384 | end 385 | 386 | test "returns default-value, when unknown-flag is passed", %{conn: conn} do 387 | {conn, client} = 388 | ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 389 | 390 | conn = ClientTestHelper.mock_feature_flags(conn, client.cache_pid) 391 | assert KindeClientSDK.get_boolean_flag(conn, "unknown_flag", false) == false 392 | end 393 | end 394 | 395 | describe "get_string_flag/2 action" do 396 | test "returns string-value, when string-flag is fetched", %{conn: conn} do 397 | {conn, client} = 398 | ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 399 | 400 | conn = ClientTestHelper.mock_feature_flags(conn, client.cache_pid) 401 | assert KindeClientSDK.get_string_flag(conn, "theme") == "grayscale" 402 | end 403 | 404 | test "returns error-message, if you try to fetch non-string flag from it", %{conn: conn} do 405 | {conn, client} = 406 | ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 407 | 408 | conn = ClientTestHelper.mock_feature_flags(conn, client.cache_pid) 409 | 410 | assert KindeClientSDK.get_string_flag(conn, "is_dark_mode") == 411 | "Error - Flag is_dark_mode is of type boolean not string" 412 | end 413 | 414 | test "returns error-message, if flag is invalid, nor doesn't exists", %{conn: conn} do 415 | {conn, client} = 416 | ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 417 | 418 | conn = ClientTestHelper.mock_feature_flags(conn, client.cache_pid) 419 | 420 | assert KindeClientSDK.get_string_flag(conn, "unknown_flag") == 421 | "Error - flag does not exist and no default provided" 422 | end 423 | end 424 | 425 | describe "get_string_flag/3 action" do 426 | test "returns string-value, when string-flag is fetched, despite of what default-value is being passed", 427 | %{conn: conn} do 428 | {conn, client} = 429 | ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 430 | 431 | conn = ClientTestHelper.mock_feature_flags(conn, client.cache_pid) 432 | assert KindeClientSDK.get_string_flag(conn, "theme", "pink") == "grayscale" 433 | end 434 | 435 | test "returns default-value, when unknown-flag is passed", %{conn: conn} do 436 | {conn, client} = 437 | ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 438 | 439 | conn = ClientTestHelper.mock_feature_flags(conn, client.cache_pid) 440 | assert KindeClientSDK.get_string_flag(conn, "unknown_flag", "pink") == "pink" 441 | end 442 | end 443 | 444 | describe "get_integer_flag/2 action" do 445 | test "returns integer-value, when integer-flag is fetched", %{conn: conn} do 446 | {conn, client} = 447 | ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 448 | 449 | conn = ClientTestHelper.mock_feature_flags(conn, client.cache_pid) 450 | assert KindeClientSDK.get_integer_flag(conn, "counter") == 55 451 | end 452 | 453 | test "returns error-message, if you try to fetch non-integer flag from it", %{conn: conn} do 454 | {conn, client} = 455 | ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 456 | 457 | conn = ClientTestHelper.mock_feature_flags(conn, client.cache_pid) 458 | 459 | assert KindeClientSDK.get_integer_flag(conn, "is_dark_mode") == 460 | "Error - Flag is_dark_mode is of type boolean not integer" 461 | end 462 | 463 | test "returns error-message, if flag is invalid + doesn't exists", %{conn: conn} do 464 | {conn, client} = 465 | ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 466 | 467 | conn = ClientTestHelper.mock_feature_flags(conn, client.cache_pid) 468 | 469 | assert KindeClientSDK.get_integer_flag(conn, "unknown_flag") == 470 | "Error - flag does not exist and no default provided" 471 | end 472 | end 473 | 474 | describe "get_integer_flag/3 action" do 475 | test "returns integer-value, when integer-flag is fetched, despite of what default-value is being passed", 476 | %{conn: conn} do 477 | {conn, client} = 478 | ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 479 | 480 | conn = ClientTestHelper.mock_feature_flags(conn, client.cache_pid) 481 | assert KindeClientSDK.get_integer_flag(conn, "counter", 99) == 55 482 | end 483 | 484 | test "returns default-value, when unknown-flag is passed", %{conn: conn} do 485 | {conn, client} = 486 | ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 487 | 488 | conn = ClientTestHelper.mock_feature_flags(conn, client.cache_pid) 489 | assert KindeClientSDK.get_integer_flag(conn, "unknown_flag", 99) == 99 490 | end 491 | end 492 | end 493 | -------------------------------------------------------------------------------- /test/pkce_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PkceTest do 2 | use ExUnit.Case 3 | 4 | alias ClientTestHelper 5 | alias KindeClientSDK 6 | alias Plug.Conn 7 | 8 | import Mock 9 | 10 | @domain Application.compile_env(:kinde_sdk, :domain) |> String.replace("\"", "") 11 | @grant_type :authorization_code_flow_pkce 12 | 13 | setup_all do 14 | conn = Plug.Test.conn(:get, "/") |> Plug.Test.init_test_session(%{}) 15 | {conn, client} = ClientTestHelper.initialize_valid_client(conn, @grant_type) 16 | {:ok, conn: conn, client: client} 17 | end 18 | 19 | test "initialize the client", %{conn: conn, client: client} do 20 | assert client.token_endpoint == @domain <> "/oauth2/token" 21 | refute is_nil(Conn.get_session(conn, :kinde_cache_pid)) 22 | end 23 | 24 | test "login", %{conn: conn, client: client} do 25 | KindeClientSDK.save_kinde_client(conn, client) 26 | 27 | conn = KindeClientSDK.login(conn, client) 28 | 29 | refute Enum.empty?(Conn.get_resp_header(conn, "location")) 30 | end 31 | 32 | test "login with audience", %{conn: conn, client: _} do 33 | additional_params = %{ 34 | audience: @domain <> "/api" 35 | } 36 | 37 | {conn, client} = 38 | ClientTestHelper.initialize_valid_client_add_params(conn, @grant_type, additional_params) 39 | 40 | KindeClientSDK.save_kinde_client(conn, client) 41 | 42 | conn = KindeClientSDK.login(conn, client) 43 | 44 | refute Enum.empty?(Conn.get_resp_header(conn, "location")) 45 | end 46 | 47 | test "login with additional", %{conn: conn, client: _} do 48 | additional_params = %{ 49 | audience: @domain <> "/api" 50 | } 51 | 52 | {conn, client} = 53 | ClientTestHelper.initialize_valid_client_add_params(conn, @grant_type, additional_params) 54 | 55 | KindeClientSDK.save_kinde_client(conn, client) 56 | 57 | additional_params_more = %{ 58 | org_code: "org_123", 59 | org_name: "My Application" 60 | } 61 | 62 | conn = KindeClientSDK.login(conn, client, additional_params_more) 63 | 64 | refute Enum.empty?(Conn.get_resp_header(conn, "location")) 65 | end 66 | 67 | test "register with additional", %{conn: conn, client: _} do 68 | additional_params = %{ 69 | audience: @domain <> "/api" 70 | } 71 | 72 | {conn, client} = 73 | ClientTestHelper.initialize_valid_client_add_params(conn, @grant_type, additional_params) 74 | 75 | KindeClientSDK.save_kinde_client(conn, client) 76 | 77 | additional_params_more = %{ 78 | org_code: "org_123", 79 | org_name: "My Application" 80 | } 81 | 82 | conn = KindeClientSDK.register(conn, client, additional_params_more) 83 | 84 | refute Enum.empty?(Conn.get_resp_header(conn, "location")) 85 | end 86 | 87 | test "create org", %{conn: conn, client: _} do 88 | additional_params = %{ 89 | audience: @domain <> "/api" 90 | } 91 | 92 | {conn, client} = 93 | ClientTestHelper.initialize_valid_client_add_params(conn, @grant_type, additional_params) 94 | 95 | KindeClientSDK.save_kinde_client(conn, client) 96 | 97 | conn = KindeClientSDK.create_org(conn, client) 98 | 99 | refute Enum.empty?(Conn.get_resp_header(conn, "location")) 100 | end 101 | 102 | test "create org with additional", %{conn: conn, client: _} do 103 | additional_params = %{ 104 | audience: @domain <> "/api" 105 | } 106 | 107 | {conn, client} = 108 | ClientTestHelper.initialize_valid_client_add_params(conn, @grant_type, additional_params) 109 | 110 | KindeClientSDK.save_kinde_client(conn, client) 111 | 112 | additional_params_more = %{ 113 | org_code: "org_123", 114 | org_name: "My Application" 115 | } 116 | 117 | conn = KindeClientSDK.create_org(conn, client, additional_params_more) 118 | 119 | refute Enum.empty?(Conn.get_resp_header(conn, "location")) 120 | end 121 | 122 | test "valid pkce login", %{conn: conn} do 123 | {conn, client} = ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 124 | 125 | conn = ClientTestHelper.mock_pkce_token(conn, client.cache_pid) 126 | assert Map.get(KindeClientSDK.get_all_data(conn), :access_token) != nil 127 | assert Map.get(KindeClientSDK.get_all_data(conn), :expires_in) != nil 128 | assert Map.get(KindeClientSDK.get_all_data(conn), :id_token) != nil 129 | assert Map.get(KindeClientSDK.get_all_data(conn), :refresh_token) != nil 130 | end 131 | 132 | test "returns old token if not expired", %{conn: conn} do 133 | {conn, client} = ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 134 | 135 | conn = ClientTestHelper.mock_pkce_token(conn, client.cache_pid) 136 | old_refresh_token = Map.get(KindeClientSDK.get_all_data(conn), :refresh_token) 137 | old_access_token = Map.get(KindeClientSDK.get_all_data(conn), :access_token) 138 | :timer.sleep(30) 139 | 140 | conn = 141 | with_mock KindeClientSDK, 142 | get_token: fn _ -> 143 | ClientTestHelper.mock_pkce_token(conn, client.cache_pid) 144 | end do 145 | KindeClientSDK.get_token(conn) 146 | end 147 | 148 | new_refresh_token = Map.get(KindeClientSDK.get_all_data(conn), :refresh_token) 149 | new_access_token = Map.get(KindeClientSDK.get_all_data(conn), :access_token) 150 | 151 | assert old_refresh_token == new_refresh_token 152 | assert old_access_token == new_access_token 153 | end 154 | 155 | test "use of refresh token to get new access-token", %{conn: conn} do 156 | {conn, client} = ClientTestHelper.init_valid_pkce_client(conn, :authorization_code_flow_pkce) 157 | 158 | conn = ClientTestHelper.mock_pkce_token(conn, client.cache_pid) 159 | old_refresh_token = Map.get(KindeClientSDK.get_all_data(conn), :refresh_token) 160 | old_access_token = Map.get(KindeClientSDK.get_all_data(conn), :access_token) 161 | :timer.sleep(70) 162 | 163 | conn = 164 | with_mock KindeClientSDK, 165 | get_token: fn _ -> 166 | ClientTestHelper.mock_result_for_refresh_token(conn, client.cache_pid) 167 | end do 168 | KindeClientSDK.get_token(conn) 169 | end 170 | 171 | new_refresh_token = Map.get(KindeClientSDK.get_all_data(conn), :refresh_token) 172 | new_access_token = Map.get(KindeClientSDK.get_all_data(conn), :access_token) 173 | 174 | assert old_refresh_token != new_refresh_token 175 | assert old_access_token != new_access_token 176 | end 177 | end 178 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | Code.require_file("client_test_helper.exs", __DIR__) 3 | -------------------------------------------------------------------------------- /test/utils_test.exs: -------------------------------------------------------------------------------- 1 | defmodule UtilsTest do 2 | use ExUnit.Case 3 | 4 | alias KindeSDK.SDK.Utils 5 | 6 | test "random string" do 7 | str = Utils.random_string(28) 8 | refute str == "" 9 | end 10 | 11 | test "generate challenge" do 12 | challenge = Utils.generate_challenge() 13 | 14 | refute is_nil(challenge[:state]) 15 | refute is_nil(challenge[:code_verifier]) 16 | refute is_nil(challenge[:code_challenge]) 17 | end 18 | 19 | test "valid url" do 20 | url = "https://test.com" 21 | assert Utils.validate_url(url) 22 | end 23 | 24 | test "invalid url" do 25 | url = "test.c" 26 | refute Utils.validate_url(url) 27 | end 28 | 29 | test "get_current_app_version/0 action returns the current-app-version" do 30 | assert Utils.get_current_app_version() == "1.2.0" 31 | end 32 | end 33 | --------------------------------------------------------------------------------