├── .bumpversion.cfg ├── .github └── workflows │ ├── builder.yml │ ├── pr-builder.yml │ └── release.yml ├── .gitignore ├── .pylintrc ├── LICENSE ├── MANIFEST ├── README.md ├── __init__.py ├── asgardeo_auth ├── Integration │ ├── __init__.py │ └── flask_client │ │ ├── __init__.py │ │ ├── flask_asgardeo_auth.py │ │ └── framework.py ├── __init__.py ├── _helpers.py ├── app_consts.py ├── asgardeo_auth.py ├── common │ ├── __init__.py │ └── security.py ├── constants │ ├── __init__.py │ ├── common.py │ ├── endpoints.py │ ├── token.py │ └── user.py ├── exception │ ├── __init__.py │ └── asgardeo_auth_error.py ├── framework.py ├── models │ ├── __init__.py │ ├── auth_config.py │ ├── authenticated_user.py │ ├── crypto.py │ ├── op_Configuration.py │ └── token_response.py ├── oidc_flow.py └── pkce.py ├── requirements.txt ├── samples └── flask │ ├── Readme.md │ ├── app.py │ ├── cert │ └── wso2.crt │ ├── conf.py │ ├── constants.py │ ├── requirements.txt │ ├── static │ ├── css │ │ └── theme.css │ └── images │ │ ├── favicon.ico │ │ ├── flask.svg │ │ ├── footer.png │ │ ├── logo-dark.svg │ │ └── oidc.png │ └── templates │ ├── dashboard.html │ └── index.html ├── setup.cfg └── setup.py /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.14-dev0 3 | commit = True 4 | tag = False 5 | allow_dirty = True 6 | parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+)(?P\d+))? 7 | serialize = 8 | {major}.{minor}.{patch}-{release}{build} 9 | {major}.{minor}.{patch} 10 | 11 | [bumpversion:file:setup.py] 12 | search = version = "{current_version}" 13 | replace = version = "{new_version}" 14 | 15 | [bumpversion:part:build] 16 | 17 | [bumpversion:part:release] 18 | optional_value = prod 19 | first_value = dev 20 | values = 21 | dev 22 | prod 23 | -------------------------------------------------------------------------------- /.github/workflows/builder.yml: -------------------------------------------------------------------------------- 1 | # This workflow builds the pushes to the master branch and bumps the version. 2 | 3 | name: Builder 4 | on: 5 | push: 6 | branches: 7 | - main 8 | paths-ignore: 9 | - 'setup.py' 10 | - '.bumpversion.cfg' 11 | - 'samples/' 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | with: 18 | token: ${{ secrets.ASGARDIO_GITHUB_BOT_TOKEN }} 19 | if: github.repository == 'asgardeo/asgardeo-auth-python-sdk' 20 | 21 | - name: Set up Python 22 | uses: actions/setup-python@v2 23 | with: 24 | python-version: '3.x' 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install flake8 pytest 29 | pip install --upgrade bump2version 30 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 31 | - name: Lint with flake8 32 | run: | 33 | # stop the build if there are Python syntax errors or undefined names 34 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 35 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 36 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 37 | - name: Bump version 38 | run: | 39 | git config --local user.email "version.bump@github.action.com" 40 | git config --local user.name "asgardeo-github-bot" 41 | bumpversion patch 42 | git push --follow-tags 43 | if: github.repository == 'asgardeo/asgardeo-auth-python-sdk' 44 | -------------------------------------------------------------------------------- /.github/workflows/pr-builder.yml: -------------------------------------------------------------------------------- 1 | name: PR Builder 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Python 13 | uses: actions/setup-python@v2 14 | with: 15 | python-version: '3.x' 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install flake8 pytest 20 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 21 | - name: Lint with flake8 22 | run: | 23 | # stop the build if there are Python syntax errors or undefined names 24 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 25 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 26 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | workflow_dispatch: 8 | inputs: 9 | releaseFlag: 10 | description: 'Are you sure you you wanted to release?' 11 | required: true 12 | default: 'No' 13 | jobs: 14 | deploy: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Python 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: '3.x' 23 | - name: Install dependencies 24 | run: | 25 | git config --local user.email "version.bump@github.action.com" 26 | git config --local user.name "asgardeo-github-bot" 27 | python -m pip install --upgrade pip 28 | pip install --upgrade bump2version 29 | pip install setuptools wheel twine 30 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 31 | - name: Build and publish 32 | id: build-and-publish 33 | env: 34 | TWINE_USERNAME: __token__ 35 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 36 | GITHUB_TOKEN: ${{ secrets.ASGARDIO_GITHUB_BOT_TOKEN }} # This token is provided by Actions, you do not need to create your own token 37 | if: "contains(github.event.inputs.releaseFlag, 'yes') || contains(github.event.inputs.releaseFlag, 'Yes') || contains(github.event.inputs.releaseFlag, 'YES')" 38 | run: | 39 | bumpversion --tag release --message '[Released] = {current_version} → {new_version}' 40 | python setup.py sdist 41 | export FOO=$(python setup.py --version) 42 | echo "::set-output name=VERSION::$FOO" 43 | twine upload dist/* 44 | 45 | - name: Create Release 46 | id: create_release 47 | uses: actions/create-release@v1 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | with: 51 | tag_name: ${{ steps.build-and-publish.outputs.VERSION }} 52 | release_name: ${{ steps.build-and-publish.outputs.VERSION }} Released ! 53 | draft: false 54 | prerelease: false 55 | 56 | - name: Upload Release Asset 57 | id: upload-release-asset 58 | uses: actions/upload-release-asset@v1 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | with: 62 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 63 | asset_path: ./dist/asgardeo-auth-python-sdk-${{steps.build-and-publish.outputs.VERSION}}.tar.gz 64 | asset_name: asgardeo-auth-python-sdk-${{steps.build-and-publish.outputs.VERSION }}.tar.gz 65 | asset_content_type: application/tar+gzip 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # IPython Notebook 7 | .ipynb_checkpoints 8 | 9 | # pyenv 10 | .python-version 11 | 12 | # celery beat schedule file 13 | celerybeat-schedule 14 | 15 | # dotenv 16 | .env 17 | 18 | # virtualenv 19 | venv/ 20 | ENV/ 21 | 22 | # Virtualenv 23 | 24 | .Python 25 | pyvenv.cfg 26 | .venv 27 | pip-selfcheck.json 28 | 29 | # virtual machine crash logs, 30 | hs_err_pid* 31 | 32 | ### JetBrains template 33 | 34 | # Gradle: 35 | .idea/gradle.xml 36 | .idea/libraries 37 | 38 | .idea/ 39 | 40 | boto3 41 | json 42 | 43 | # Log file 44 | *.log 45 | 46 | # BlueJ files 47 | *.ctxt 48 | 49 | # Package Files # 50 | *.jar 51 | *.war 52 | *.nar 53 | *.ear 54 | *.zip 55 | *.tar.gz 56 | *.rar 57 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | # This Pylint rcfile contains a best-effort configuration to uphold the 2 | # best-practices and style described in the Google Python style guide: 3 | # https://google.github.io/styleguide/pyguide.html 4 | # 5 | # Its canonical open-source location is: 6 | # https://google.github.io/styleguide/pylintrc 7 | 8 | [MASTER] 9 | 10 | # Add files or directories to the blacklist. They should be base names, not 11 | # paths. 12 | ignore=third_party 13 | 14 | # Add files or directories matching the regex patterns to the blacklist. The 15 | # regex matches against base names, not paths. 16 | ignore-patterns= 17 | 18 | # Pickle collected data for later comparisons. 19 | persistent=no 20 | 21 | # List of plugins (as comma separated values of python modules names) to load, 22 | # usually to register additional checkers. 23 | load-plugins= 24 | 25 | # Use multiple processes to speed up Pylint. 26 | jobs=4 27 | 28 | # Allow loading of arbitrary C extensions. Extensions are imported into the 29 | # active Python interpreter and may run arbitrary code. 30 | unsafe-load-any-extension=no 31 | 32 | # A comma-separated list of package or module names from where C extensions may 33 | # be loaded. Extensions are loading into the active Python interpreter and may 34 | # run arbitrary code 35 | extension-pkg-whitelist= 36 | 37 | 38 | [MESSAGES CONTROL] 39 | 40 | # Only show warnings with the listed confidence levels. Leave empty to show 41 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 42 | confidence= 43 | 44 | # Enable the message, report, category or checker with the given id(s). You can 45 | # either give multiple identifier separated by comma (,) or put this option 46 | # multiple time (only on the command line, not in the configuration file where 47 | # it should appear only once). See also the "--disable" option for examples. 48 | #enable= 49 | 50 | # Disable the message, report, category or checker with the given id(s). You 51 | # can either give multiple identifiers separated by comma (,) or put this 52 | # option multiple times (only on the command line, not in the configuration 53 | # file where it should appear only once).You can also use "--disable=all" to 54 | # disable everything first and then reenable specific checks. For example, if 55 | # you want to run only the similarities checker, you can use "--disable=all 56 | # --enable=similarities". If you want to run only the classes checker, but have 57 | # no Warning level messages displayed, use"--disable=all --enable=classes 58 | # --disable=W" 59 | disable=abstract-method, 60 | apply-builtin, 61 | arguments-differ, 62 | attribute-defined-outside-init, 63 | backtick, 64 | bad-option-value, 65 | basestring-builtin, 66 | buffer-builtin, 67 | c-extension-no-member, 68 | consider-using-enumerate, 69 | cmp-builtin, 70 | cmp-method, 71 | coerce-builtin, 72 | coerce-method, 73 | delslice-method, 74 | div-method, 75 | duplicate-code, 76 | eq-without-hash, 77 | execfile-builtin, 78 | file-builtin, 79 | filter-builtin-not-iterating, 80 | fixme, 81 | getslice-method, 82 | global-statement, 83 | hex-method, 84 | idiv-method, 85 | implicit-str-concat-in-sequence, 86 | import-error, 87 | import-self, 88 | import-star-module-level, 89 | inconsistent-return-statements, 90 | input-builtin, 91 | intern-builtin, 92 | invalid-str-codec, 93 | locally-disabled, 94 | long-builtin, 95 | long-suffix, 96 | map-builtin-not-iterating, 97 | misplaced-comparison-constant, 98 | missing-function-docstring, 99 | metaclass-assignment, 100 | next-method-called, 101 | next-method-defined, 102 | no-absolute-import, 103 | no-else-break, 104 | no-else-continue, 105 | no-else-raise, 106 | no-else-return, 107 | no-init, # added 108 | no-member, 109 | no-name-in-module, 110 | no-self-use, 111 | nonzero-method, 112 | oct-method, 113 | old-division, 114 | old-ne-operator, 115 | old-octal-literal, 116 | old-raise-syntax, 117 | parameter-unpacking, 118 | print-statement, 119 | raising-string, 120 | range-builtin-not-iterating, 121 | raw_input-builtin, 122 | rdiv-method, 123 | reduce-builtin, 124 | relative-import, 125 | reload-builtin, 126 | round-builtin, 127 | setslice-method, 128 | signature-differs, 129 | standarderror-builtin, 130 | suppressed-message, 131 | sys-max-int, 132 | too-few-public-methods, 133 | too-many-ancestors, 134 | too-many-arguments, 135 | too-many-boolean-expressions, 136 | too-many-branches, 137 | too-many-instance-attributes, 138 | too-many-locals, 139 | too-many-nested-blocks, 140 | too-many-public-methods, 141 | too-many-return-statements, 142 | too-many-statements, 143 | trailing-newlines, 144 | unichr-builtin, 145 | unicode-builtin, 146 | unnecessary-pass, 147 | unpacking-in-except, 148 | useless-else-on-loop, 149 | useless-object-inheritance, 150 | useless-suppression, 151 | using-cmp-argument, 152 | wrong-import-order, 153 | xrange-builtin, 154 | zip-builtin-not-iterating, 155 | 156 | 157 | [REPORTS] 158 | 159 | # Set the output format. Available formats are text, parseable, colorized, msvs 160 | # (visual studio) and html. You can also give a reporter class, eg 161 | # mypackage.mymodule.MyReporterClass. 162 | output-format=text 163 | 164 | # Put messages in a separate file for each module / package specified on the 165 | # command line instead of printing them on stdout. Reports (if any) will be 166 | # written in a file name "pylint_global.[txt|html]". This option is deprecated 167 | # and it will be removed in Pylint 2.0. 168 | files-output=no 169 | 170 | # Tells whether to display a full report or only the messages 171 | reports=no 172 | 173 | # Python expression which should return a note less than 10 (10 is the highest 174 | # note). You have access to the variables errors warning, statement which 175 | # respectively contain the number of errors / warnings messages and the total 176 | # number of statements analyzed. This is used by the global evaluation report 177 | # (RP0004). 178 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 179 | 180 | # Template used to display messages. This is a python new-style format string 181 | # used to format the message information. See doc for all details 182 | #msg-template= 183 | 184 | 185 | [BASIC] 186 | 187 | # Good variable names which should always be accepted, separated by a comma 188 | good-names=main,_ 189 | 190 | # Bad variable names which should always be refused, separated by a comma 191 | bad-names= 192 | 193 | # Colon-delimited sets of names that determine each other's naming style when 194 | # the name regexes allow several styles. 195 | name-group= 196 | 197 | # Include a hint for the correct naming format with invalid-name 198 | include-naming-hint=no 199 | 200 | # List of decorators that produce properties, such as abc.abstractproperty. Add 201 | # to this list to register other decorators that produce valid properties. 202 | property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl 203 | 204 | # Regular expression matching correct function names 205 | function-rgx=^(?:(?PsetUp|tearDown|setUpModule|tearDownModule)|(?P_?[A-Z][a-zA-Z0-9]*)|(?P_?[a-z][a-z0-9_]*))$ 206 | 207 | # Regular expression matching correct variable names 208 | variable-rgx=^[a-z][a-z0-9_]*$ 209 | 210 | # Regular expression matching correct constant names 211 | const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ 212 | 213 | # Regular expression matching correct attribute names 214 | attr-rgx=^_{0,2}[a-z][a-z0-9_]*$ 215 | 216 | # Regular expression matching correct argument names 217 | argument-rgx=^[a-z][a-z0-9_]*$ 218 | 219 | # Regular expression matching correct class attribute names 220 | class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ 221 | 222 | # Regular expression matching correct inline iteration names 223 | inlinevar-rgx=^[a-z][a-z0-9_]*$ 224 | 225 | # Regular expression matching correct class names 226 | class-rgx=^_?[A-Z][a-zA-Z0-9]*$ 227 | 228 | # Regular expression matching correct module names 229 | module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$ 230 | 231 | # Regular expression matching correct method names 232 | method-rgx=(?x)^(?:(?P_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P_{0,2}[a-z][a-z0-9_]*))$ 233 | 234 | # Regular expression which should only match function or class names that do 235 | # not require a docstring. 236 | no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$ 237 | 238 | # Minimum line length for functions/classes that require docstrings, shorter 239 | # ones are exempt. 240 | docstring-min-length=10 241 | 242 | 243 | [TYPECHECK] 244 | 245 | # List of decorators that produce context managers, such as 246 | # contextlib.contextmanager. Add to this list to register other decorators that 247 | # produce valid context managers. 248 | contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager 249 | 250 | # Tells whether missing members accessed in mixin class should be ignored. A 251 | # mixin class is detected if its name ends with "mixin" (case insensitive). 252 | ignore-mixin-members=yes 253 | 254 | # List of module names for which member attributes should not be checked 255 | # (useful for modules/projects where namespaces are manipulated during runtime 256 | # and thus existing member attributes cannot be deduced by static analysis. It 257 | # supports qualified module names, as well as Unix pattern matching. 258 | ignored-modules= 259 | 260 | # List of class names for which member attributes should not be checked (useful 261 | # for classes with dynamically set attributes). This supports the use of 262 | # qualified names. 263 | ignored-classes=optparse.Values,thread._local,_thread._local 264 | 265 | # List of members which are set dynamically and missed by pylint inference 266 | # system, and so shouldn't trigger E1101 when accessed. Python regular 267 | # expressions are accepted. 268 | generated-members= 269 | 270 | 271 | [FORMAT] 272 | 273 | # Maximum number of characters on a single line. 274 | max-line-length=80 275 | 276 | # TODO(https://github.com/PyCQA/pylint/issues/3352): Direct pylint to exempt 277 | # lines made too long by directives to pytype. 278 | 279 | # Regexp for a line that is allowed to be longer than the limit. 280 | ignore-long-lines=(?x)( 281 | ^\s*(\#\ )??$| 282 | ^\s*(from\s+\S+\s+)?import\s+.+$) 283 | 284 | # Allow the body of an if to be on the same line as the test if there is no 285 | # else. 286 | single-line-if-stmt=yes 287 | 288 | # List of optional constructs for which whitespace checking is disabled. `dict- 289 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 290 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 291 | # `empty-line` allows space-only lines. 292 | no-space-check= 293 | 294 | # Maximum number of lines in a module 295 | max-module-lines=99999 296 | 297 | # String used as indentation unit. The internal Google style guide mandates 2 298 | # spaces. Google's externaly-published style guide says 4, consistent with 299 | # PEP 8. Here, we use 2 spaces, for conformity with many open-sourced Google 300 | # projects (like TensorFlow). 301 | indent-string=' ' 302 | 303 | # Number of spaces of indent required inside a hanging or continued line. 304 | indent-after-paren=4 305 | 306 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 307 | expected-line-ending-format= 308 | 309 | 310 | [MISCELLANEOUS] 311 | 312 | # List of note tags to take in consideration, separated by a comma. 313 | notes=TODO 314 | 315 | 316 | [STRING] 317 | 318 | # This flag controls whether inconsistent-quotes generates a warning when the 319 | # character used as a quote delimiter is used inconsistently within a module. 320 | check-quote-consistency=yes 321 | 322 | 323 | [VARIABLES] 324 | 325 | # Tells whether we should check for unused import in __init__ files. 326 | init-import=no 327 | 328 | # A regular expression matching the name of dummy variables (i.e. expectedly 329 | # not used). 330 | dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_) 331 | 332 | # List of additional names supposed to be defined in builtins. Remember that 333 | # you should avoid to define new builtins when possible. 334 | additional-builtins= 335 | 336 | # List of strings which can identify a callback function by name. A callback 337 | # name must start or end with one of those strings. 338 | callbacks=cb_,_cb 339 | 340 | # List of qualified module names which can have objects that can redefine 341 | # builtins. 342 | redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools 343 | 344 | 345 | [LOGGING] 346 | 347 | # Logging modules to check that the string format arguments are in logging 348 | # function parameter format 349 | logging-modules=logging,absl.logging,tensorflow.io.logging 350 | 351 | 352 | [SIMILARITIES] 353 | 354 | # Minimum lines number of a similarity. 355 | min-similarity-lines=4 356 | 357 | # Ignore comments when computing similarities. 358 | ignore-comments=yes 359 | 360 | # Ignore docstrings when computing similarities. 361 | ignore-docstrings=yes 362 | 363 | # Ignore imports when computing similarities. 364 | ignore-imports=no 365 | 366 | 367 | [SPELLING] 368 | 369 | # Spelling dictionary name. Available dictionaries: none. To make it working 370 | # install python-enchant package. 371 | spelling-dict= 372 | 373 | # List of comma separated words that should not be checked. 374 | spelling-ignore-words= 375 | 376 | # A path to a file that contains private dictionary; one word per line. 377 | spelling-private-dict-file= 378 | 379 | # Tells whether to store unknown words to indicated private dictionary in 380 | # --spelling-private-dict-file option instead of raising a message. 381 | spelling-store-unknown-words=no 382 | 383 | 384 | [IMPORTS] 385 | 386 | # Deprecated modules which should not be used, separated by a comma 387 | deprecated-modules=regsub, 388 | TERMIOS, 389 | Bastion, 390 | rexec, 391 | sets 392 | 393 | # Create a graph of every (i.e. internal and external) dependencies in the 394 | # given file (report RP0402 must not be disabled) 395 | import-graph= 396 | 397 | # Create a graph of external dependencies in the given file (report RP0402 must 398 | # not be disabled) 399 | ext-import-graph= 400 | 401 | # Create a graph of internal dependencies in the given file (report RP0402 must 402 | # not be disabled) 403 | int-import-graph= 404 | 405 | # Force import order to recognize a module as part of the standard 406 | # compatibility libraries. 407 | known-standard-library= 408 | 409 | # Force import order to recognize a module as part of a third party library. 410 | known-third-party=enchant, absl 411 | 412 | # Analyse import fallback blocks. This can be used to support both Python 2 and 413 | # 3 compatible code, which means that the block might have code that exists 414 | # only in one or another interpreter, leading to false positives when analysed. 415 | analyse-fallback-blocks=no 416 | 417 | 418 | [CLASSES] 419 | 420 | # List of method names used to declare (i.e. assign) instance attributes. 421 | defining-attr-methods=__init__, 422 | __new__, 423 | setUp 424 | 425 | # List of member names, which should be excluded from the protected access 426 | # warning. 427 | exclude-protected=_asdict, 428 | _fields, 429 | _replace, 430 | _source, 431 | _make 432 | 433 | # List of valid names for the first argument in a class method. 434 | valid-classmethod-first-arg=cls, 435 | class_ 436 | 437 | # List of valid names for the first argument in a metaclass class method. 438 | valid-metaclass-classmethod-first-arg=mcs 439 | 440 | 441 | [EXCEPTIONS] 442 | 443 | # Exceptions that will emit a warning when being caught. Defaults to 444 | # "Exception" 445 | overgeneral-exceptions=StandardError, 446 | Exception, 447 | BaseException -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | setup.cfg 3 | setup.py 4 | asgardeo_auth_python_sdk/__init__.py 5 | asgardeo_auth_python_sdk/_helpers.py 6 | asgardeo_auth_python_sdk/app_consts.py 7 | asgardeo_auth_python_sdk/asgardeo_auth.py 8 | asgardeo_auth_python_sdk/framework.py 9 | asgardeo_auth_python_sdk/oidc_flow.py 10 | asgardeo_auth_python_sdk/pkce.py 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [DEPRECATED] Asgardeo-auth-python-sdk 2 | 3 | ## :warning: Warning! 4 | ### Python SDK is no longer encouraged and enriched by Asgardeo and may not work with the latest Python versions. 5 | ### You can implement login using [Authorization Code flow](https://wso2.com/asgardeo/docs/guides/authentication/oidc/implement-auth-code/#prerequisites) with Asgardeo using OIDC standards. 6 | --- 7 | 8 | ![Builder](https://github.com/asgardeo/asgardeo-auth-python-sdk/workflows/Builder/badge.svg) 9 | [![Downloads](https://pepy.tech/badge/asgardeo-auth-python-sdk)](https://pepy.tech/project/asgardeo-auth-python-sdk) 10 | [![Stackoverflow](https://img.shields.io/badge/Ask%20for%20help%20on-Stackoverflow-orange)](https://stackoverflow.com/questions/tagged/asgardeo) 11 | [![Discord](https://img.shields.io/badge/Join%20us%20on-Discord-%23e01563.svg)](https://discord.gg/wso2) 12 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/asgardeo/asgardeo-auth-python-sdk/blob/main/LICENSE) 13 | 14 | 15 | ## Table of Content 16 | 17 | - [Introduction](#introduction) 18 | - [Prerequisite](#prerequisite) 19 | - [Try Out the Sample Apps](#try-out-the-sample-apps) 20 | - [Getting Started](#getting-started) 21 | - [Develop](#develop) 22 | - [Contribute](#contribute) 23 | - [License](#license) 24 | 25 | ## Introduction 26 | 27 | Asgardeo Auth Python SDK provides the core methods that are needed to implement OIDC authentication in Python based apps. This SDK can be used to build SDKs for Web Applications from different frameworks such as Flask, Django and various other frameworks that use Python. By using Asgardeo and the Asgardeo Auth Python SDK, developers will be able to add identity management to their Python based applications fast and secure. 28 | 29 | To enable authentication for the sample application, we are using Asgardeo as the Identity Provider. 30 | 31 | ## Prerequisite 32 | 33 | Create an organization in Asgardeo if you don't already have one. The organization name you choose will be referred to as `` throughout this document. 34 | 35 | ## Try Out the Sample Apps 36 | 37 | ### 1. Create an Application in Asgardeo 38 | 39 | Before trying out the sample apps, you need to create an application in **Asgardeo**. 40 | 41 | 1. Navigate to [**Asgardeo Console**](https://console.asgardeo.io/login) and click on **Applications** under **Develop** tab. 42 | 43 | 2. Click on **New Application** and then **Traditional Web Application**. 44 | 45 | 3. Enter **Sample** as the name of the app and add the redirect URL(s). You can find the relevant redirect URL(s) of each sample app in the [Running the sample apps](#2-running-the-sample-apps) section. 46 | 47 | 4. Click on Register. You will be navigated to management page of the **Sample** application. 48 | 49 | 5. Add `https://localhost:3000` (or whichever the URL your app is hosted on) to **Allowed Origins** under **Protocol** tab. 50 | 51 | 6. Click on **Update** at the bottom. 52 | 53 | 54 | ### 2. Running the sample apps 55 | 56 | 1. Fork and clone [python-sdk repo](https://github.com/asgardeo/asgardeo-auth-python-sdk) 57 | 58 | 2. Update configuration file `conf.py` with your registered app details. 59 | 60 | ```python 61 | auth_config = { 62 | "login_callback_url": "https://localhost:3000/login", 63 | "logout_callback_url": "https://localhost:3000/signin", 64 | "client_host": "https://localhost:3000", 65 | "client_id": "", 66 | "client_secret": "", 67 | "server_origin": "https://api.asgardeo.io", 68 | "tenant_path": "/t/", 69 | "tenant": "", 70 | "certificate_path": "cert/wso2.crt" 71 | } 72 | ``` 73 | 74 | 3. Obtain an [SSL certificate](https://www.globalsign.com/en/blog/how-to-view-ssl-certificate-details) for [https://console.asgardeo.io/](https://console.asgardeo.io/) and replace the content of wso2.crt with the obtained one. 75 | 76 | 4. Run `pip3 install -r requirements.txt` 77 | 78 | 5. Run the application. 79 | 80 | 6. Navigate to `https://localhost:3000` (or whichever the URL you have hosted the sample app) from the browser. 81 | 82 | #### Basic Flask Sample 83 | 84 | - Download the Sample: [samples/flask](https://github.com/asgardeo/asgardeo-auth-python-sdk/tree/main/samples/flask) 85 | 86 | - Find More Info: [README](/samples/flask/Readme.md) 87 | 88 | - **Redirect URL(s):** 89 | - `https://localhost:3000/login` 90 | - `https://localhost:3000/signin` 91 | 92 | 93 | 94 | ## Getting Started 95 | 96 | ### 1. Install the library from PyPI. 97 | 98 | ``` 99 | pip install asgardeo-auth-python-sdk 100 | ``` 101 | 102 | ### 2. Set up the application using the provided APIs 103 | Python Authentication SDK is architectured in a way that any python framework could be integrated with the Core SDK 104 | . Currently the SDK itself supports Flask framework. 105 | you can find the documentation [here](https://github.com/asgardeo/asgardeo-auth-python-sdk/tree/main/samples/flask/Readme.md). 106 | 107 | Still you can implement your own way of implementation using the APIs provided by the core. 108 | 109 | ## Develop 110 | 111 | ### Prerequisites 112 | 113 | - `Python` 114 | - `pip` package manager. 115 | 116 | ### Installing Dependencies 117 | 118 | The repository is a mono repository. The SDK repository is found in the [asgardeo_auth](https://github.com/asgardeo/asgardeo-auth-python-sdk/tree/main/asgardeo_auth) directory. You can install the dependencies by running the following command at the root. 119 | 120 | ``` 121 | pip3 install -r requirements.txt 122 | ``` 123 | ## Contribute 124 | 125 | Please read [Contributing to the Code Base](http://wso2.github.io/) for details on our code of conduct, and the process for submitting pull requests to us. 126 | 127 | ### Reporting issues 128 | 129 | We encourage you to report issues, improvements, and feature requests creating [Github Issues](https://github.com/asgardeo/asgardeo-auth-python-sdk/issues). 130 | 131 | Important: And please be advised that security issues must be reported to security@wso2com, not as GitHub issues, in order to reach the proper audience. We strongly advise following the WSO2 Security Vulnerability Reporting Guidelines when reporting the security issues. 132 | 133 | ## License 134 | 135 | This project is licensed under the Apache License 2.0. See the [LICENSE](https://github.com/asgardeo/asgardeo-auth-python-sdk/blob/main/LICENSE) file for details. 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wso2-attic/asgardeo-auth-python-sdk/87dba49f522a8e0192dcbd5c9a9bb2503a930949/__init__.py -------------------------------------------------------------------------------- /asgardeo_auth/Integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wso2-attic/asgardeo-auth-python-sdk/87dba49f522a8e0192dcbd5c9a9bb2503a930949/asgardeo_auth/Integration/__init__.py -------------------------------------------------------------------------------- /asgardeo_auth/Integration/flask_client/__init__.py: -------------------------------------------------------------------------------- 1 | from .flask_asgardeo_auth import FlaskAsgardeoAuth 2 | -------------------------------------------------------------------------------- /asgardeo_auth/Integration/flask_client/flask_asgardeo_auth.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from flask import request as flask_req 4 | from flask import session, redirect 5 | 6 | from .framework import FlaskFramework 7 | from ... import AsgardeoAuth, AsgardeoAuthError 8 | from ...constants.common import TOKEN_RESPONSE, REDIRECT 9 | from ...constants.token import ACCESS_TOKEN, ID_TOKEN, AUTHORIZATION_CODE, \ 10 | STATE 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class FlaskAsgardeoAuth(AsgardeoAuth): 16 | """ 17 | IdentityAuth class. 18 | """ 19 | 20 | def __init__(self, auth_config): 21 | framework = FlaskFramework() 22 | super().__init__(auth_config, framework) 23 | 24 | def prepare_params_for_workflow(self): 25 | 26 | constructor_kwargs = dict( 27 | redirect_uri=self.auth_config["logout_callback_url"], 28 | op_configuration=self.op_configuration, 29 | pkce=self.auth_config["enable_pkce"], 30 | prompt=self.auth_config["prompt"], code_verifier=None) 31 | return constructor_kwargs 32 | 33 | def sign_in(self): 34 | 35 | result = {} 36 | if self.framework.is_session_data_available(flask_req, ACCESS_TOKEN) \ 37 | and self.framework.is_session_data_available(flask_req, 38 | ID_TOKEN): 39 | if self.op_configuration.is_valid_op_config( 40 | self.auth_config.tenant): 41 | result[REDIRECT] = self.auth_config.client_host 42 | return result 43 | else: 44 | result[TOKEN_RESPONSE] = self.oidc_flow.get_authenticated_user( 45 | self.framework.get_session_data(flask_req, ID_TOKEN)) 46 | return result 47 | else: 48 | code = self.get_authorization_code(flask_req) 49 | response = self.send_sign_in_request(flask_req, code) 50 | return response 51 | 52 | def get_authorization_code(self, request): 53 | """Retrieve parameters for fetching access token, those parameters come 54 | from request and previously saved temporary data in session. 55 | """ 56 | code = None 57 | request_state = None 58 | if request.method == 'GET': 59 | if request.args.get(AUTHORIZATION_CODE): 60 | code = request.args[AUTHORIZATION_CODE] 61 | request_state = request.args.get(STATE) 62 | elif request.method == 'POST': 63 | if request.form and request.form.get(AUTHORIZATION_CODE): 64 | code = request.form[AUTHORIZATION_CODE] 65 | request_state = request.form.get(STATE) 66 | if code: 67 | self.validate_state_param(request, request_state) 68 | return code 69 | 70 | if AUTHORIZATION_CODE in session: 71 | return session[AUTHORIZATION_CODE] 72 | 73 | return None 74 | 75 | def validate_state_param(self, request, request_state): 76 | 77 | state = self.framework.get_session_data(request, STATE) 78 | if state != request_state: 79 | raise AsgardeoAuthError( 80 | "CSRF Warning! State not equal in request and response.") 81 | 82 | def send_refresh_token_request(self, refresh_token): 83 | self.oidc_flow.send_refresh_token_request(refresh_token) 84 | 85 | def sign_out(self): 86 | return redirect(self.send_sign_out_request()) 87 | 88 | def is_session_data_available(self, key): 89 | return self.framework.is_session_data_available(None, key) 90 | -------------------------------------------------------------------------------- /asgardeo_auth/Integration/flask_client/framework.py: -------------------------------------------------------------------------------- 1 | from flask import session 2 | 3 | from ...framework import Framework 4 | 5 | 6 | class FlaskFramework(Framework): 7 | 8 | def __init__(self, name="wso2_is {}"): 9 | self.name = name 10 | 11 | def set_session_data(self, request, key, value): 12 | sess_key = self.name.format(key) 13 | session[sess_key] = value 14 | 15 | def get_session_data(self, request, key): 16 | sess_key = self.name.format(key) 17 | return session.get(sess_key, None) 18 | 19 | def is_session_data_available(self, request, key): 20 | sess_key = self.name.format(key) 21 | return sess_key in session 22 | 23 | def clear_session_data(self, request, key): 24 | sess_key = self.name.format(key) 25 | session.pop(sess_key, None) 26 | -------------------------------------------------------------------------------- /asgardeo_auth/__init__.py: -------------------------------------------------------------------------------- 1 | from .asgardeo_auth import AsgardeoAuth 2 | from .constants.common import * 3 | from .constants.user import * 4 | from .exception.asgardeo_auth_error import AsgardeoAuthError -------------------------------------------------------------------------------- /asgardeo_auth/_helpers.py: -------------------------------------------------------------------------------- 1 | """Helper functions for commonly used utilities.""" 2 | import base64 3 | import json 4 | import logging 5 | import os 6 | import warnings 7 | 8 | import six 9 | from six.moves import urllib 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | POSITIONAL_WARNING = 'WARNING' 14 | POSITIONAL_EXCEPTION = 'EXCEPTION' 15 | POSITIONAL_IGNORE = 'IGNORE' 16 | POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION, 17 | POSITIONAL_IGNORE]) 18 | 19 | positional_parameters_enforcement = POSITIONAL_WARNING 20 | 21 | _SYM_LINK_MESSAGE = 'File: {0}: Is a symbolic link.' 22 | _IS_DIR_MESSAGE = '{0}: Is a directory' 23 | _MISSING_FILE_MESSAGE = 'Cannot access {0}: No such file or directory' 24 | 25 | 26 | def scopes_to_string(scopes): 27 | """Converts scope value to a string. 28 | 29 | If scopes is a string then it is simply passed through. If scopes is an 30 | iterable then a string is returned that is all the individual scopes 31 | concatenated with spaces. 32 | 33 | Args: 34 | scopes: string or iterable of strings, the scopes. 35 | 36 | Returns: 37 | The scopes formatted as a single string. 38 | """ 39 | if isinstance(scopes, six.string_types): 40 | return scopes 41 | else: 42 | return ' '.join(scopes) 43 | 44 | 45 | def string_to_scopes(scopes): 46 | """Converts stringifed scope value to a list. 47 | 48 | If scopes is a list then it is simply passed through. If scopes is an 49 | string then a list of each individual scope is returned. 50 | 51 | Args: 52 | scopes: a string or iterable of strings, the scopes. 53 | 54 | Returns: 55 | The scopes in a list. 56 | """ 57 | if not scopes: 58 | return [] 59 | elif isinstance(scopes, six.string_types): 60 | return scopes.split(' ') 61 | else: 62 | return scopes 63 | 64 | 65 | def parse_unique_urlencoded(content): 66 | """Parses unique key-value parameters from urlencoded content. 67 | 68 | Args: 69 | content: string, URL-encoded key-value pairs. 70 | 71 | Returns: 72 | dict, The key-value pairs from ``content``. 73 | 74 | Raises: 75 | ValueError: if one of the keys is repeated. 76 | """ 77 | urlencoded_params = urllib.parse.parse_qs(content) 78 | params = {} 79 | for key, value in six.iteritems(urlencoded_params): 80 | if len(value) != 1: 81 | msg = ('URL-encoded content contains a repeated value:' 82 | '%s -> %s' % (key, ', '.join(value))) 83 | raise ValueError(msg) 84 | params[key] = value[0] 85 | return params 86 | 87 | 88 | def update_query_params(uri, params): 89 | """Updates a URI with new query parameters. 90 | 91 | If a given key from ``params`` is repeated in the ``uri``, then 92 | the URI will be considered invalid and an error will occur. 93 | 94 | If the URI is valid, then each value from ``params`` will 95 | replace the corresponding value in the query parameters (if 96 | it exists). 97 | 98 | Args: 99 | uri: string, A valid URI, with potential existing query parameters. 100 | params: dict, A dictionary of query parameters. 101 | 102 | Returns: 103 | The same URI but with the new query parameters added. 104 | """ 105 | parts = urllib.parse.urlparse(uri) 106 | query_params = parse_unique_urlencoded(parts.query) 107 | query_params.update(params) 108 | new_query = urllib.parse.urlencode(query_params) 109 | new_parts = parts._replace(query=new_query) 110 | return urllib.parse.urlunparse(new_parts) 111 | 112 | 113 | def validate_file(filename): 114 | if os.path.islink(filename): 115 | raise IOError(_SYM_LINK_MESSAGE.format(filename)) 116 | elif os.path.isdir(filename): 117 | raise IOError(_IS_DIR_MESSAGE.format(filename)) 118 | elif not os.path.isfile(filename): 119 | warnings.warn(_MISSING_FILE_MESSAGE.format(filename)) 120 | 121 | 122 | def _to_bytes(value, encoding='ascii'): 123 | """Converts a string value to bytes, if necessary. 124 | 125 | Unfortunately, ``six.b`` is insufficient for this task since in 126 | Python2 it does not modify ``unicode`` objects. 127 | 128 | Args: 129 | value: The string/bytes value to be converted. 130 | encoding: The encoding to use to convert unicode to bytes. Defaults 131 | to "ascii", which will not allow any characters from ordinals 132 | larger than 127. Other useful values are "latin-1", which 133 | which will only allows byte ordinals (up to 255) and "utf-8", 134 | which will encode any unicode that needs to be. 135 | 136 | Returns: 137 | The original value converted to bytes (if unicode) or as passed in 138 | if it started out as bytes. 139 | 140 | Raises: 141 | ValueError if the value could not be converted to bytes. 142 | """ 143 | result = (value.encode(encoding) 144 | if isinstance(value, six.text_type) else value) 145 | if isinstance(result, six.binary_type): 146 | return result 147 | else: 148 | raise ValueError('{0!r} could not be converted to bytes'.format(value)) 149 | 150 | 151 | def _from_bytes(value): 152 | """Converts bytes to a string value, if necessary. 153 | 154 | Args: 155 | value: The string/bytes value to be converted. 156 | 157 | Returns: 158 | The original value converted to unicode (if bytes) or as passed in 159 | if it started out as unicode. 160 | 161 | Raises: 162 | ValueError if the value could not be converted to unicode. 163 | """ 164 | result = (value.decode('utf-8') 165 | if isinstance(value, six.binary_type) else value) 166 | if isinstance(result, six.text_type): 167 | return result 168 | else: 169 | raise ValueError( 170 | '{0!r} could not be converted to unicode'.format(value)) 171 | 172 | 173 | def _urlsafe_b64encode(raw_bytes): 174 | raw_bytes = _to_bytes(raw_bytes, encoding='utf-8') 175 | return base64.urlsafe_b64encode(raw_bytes).rstrip(b'=') 176 | 177 | 178 | def _urlsafe_b64decode(b64string): 179 | # Guard against unicode strings, which base64 can't handle. 180 | b64string = _to_bytes(b64string) 181 | padded = b64string + b'=' * (4 - len(b64string) % 4) 182 | return base64.urlsafe_b64decode(padded) 183 | 184 | 185 | def parse_exchange_token_response(content): 186 | """Parses response of an exchange token request. 187 | 188 | Most providers return JSON but some (e.g. Facebook) return a 189 | url-encoded string. 190 | 191 | Args: 192 | content: The body of a response 193 | 194 | Returns: 195 | Content as a dictionary object. Note that the dict could be empty, 196 | i.e. {}. That basically indicates a failure. 197 | """ 198 | resp = {} 199 | content = _from_bytes(content) 200 | try: 201 | resp = json.loads(content) 202 | except Exception: 203 | # different JSON libs raise different exceptions, 204 | # so we just do a catch-all here 205 | resp = parse_unique_urlencoded(content) 206 | 207 | # some providers respond with 'expires', others with 'expires_in' 208 | if resp and 'expires' in resp: 209 | resp['expires_in'] = resp.pop('expires') 210 | 211 | return resp 212 | -------------------------------------------------------------------------------- /asgardeo_auth/app_consts.py: -------------------------------------------------------------------------------- 1 | name = 'asgardeo-auth-python-sdk' 2 | packages = ('asgardeo_auth', 'asgardeo_auth.*') 3 | version = "0.1.3-dev0" 4 | author = 'Asgardeo' 5 | homepage = 'https://github.com/asgardeo/asgardeo-auth-python-sdk#readme' 6 | license_name = 'Apache Software License' 7 | description = "Asgardeo Auth Python SDK." 8 | bug_tracker = 'https://github.com/asgardeo/asgardeo-auth-python-sdk/issues' 9 | keywords = [ 10 | "Asgardeo", 11 | "OIDC", 12 | "OAuth2", 13 | "Authentication", 14 | "Authorization" 15 | ] 16 | download_url = 'https://github.com/asgardeo/asgardeo-auth-python-sdk/releases' 17 | author = "Asgardeo", 18 | author_email = "beta@asgardeo.io" 19 | default_user_agent = '{}/{} (+{})'.format(name, version, homepage) 20 | default_json_headers = [ 21 | ('Content-Type', 'application/json'), 22 | ('Cache-Control', 'no-store'), 23 | ('Pragma', 'no-cache'), 24 | ] 25 | -------------------------------------------------------------------------------- /asgardeo_auth/asgardeo_auth.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from .constants.common import AUTHORIZATION_CODE_TYPE, LOGIN_SCOPE, \ 4 | DEFAULT_SUPER_TENANT, TOKEN_RESPONSE, REDIRECT, URL, USER 5 | from .constants.token import STATE, ACCESS_TOKEN, ID_TOKEN, ID_TOKEN_JWT 6 | from .constants.user import USERNAME 7 | from .exception.asgardeo_auth_error import AsgardeoAuthError 8 | from .models.auth_config import AuthConfig 9 | from .models.authenticated_user import AuthenticatedUser 10 | from .models.op_Configuration import OPConfiguration 11 | from .models.token_response import TokenResponse 12 | from .oidc_flow import OIDCFlow 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | DefaultConfig = { 17 | "authorization_type": AUTHORIZATION_CODE_TYPE, 18 | "client_secret": None, 19 | "consent_denied": False, 20 | "enable_pkce": True, 21 | "response_mode": None, 22 | "scope": [LOGIN_SCOPE], 23 | "tenant": DEFAULT_SUPER_TENANT, 24 | "tenant_path": "", 25 | "prompt": "" 26 | } 27 | 28 | post_auth_session_keys = [ 29 | USER, ACCESS_TOKEN, ID_TOKEN, ID_TOKEN_JWT, 30 | USERNAME 31 | ] 32 | 33 | 34 | class IdentityAuthBase(type): 35 | """ 36 | Define an Instance operation that lets clients access its unique 37 | instance. 38 | """ 39 | 40 | def __init__(cls, name, bases, attrs, **kwargs): 41 | super().__init__(name, bases, attrs) 42 | cls._instance = None 43 | 44 | def __call__(cls, *args, **kwargs): 45 | if cls._instance is None: 46 | cls._instance = super().__call__(*args, **kwargs) 47 | return cls._instance 48 | 49 | 50 | class AsgardeoAuth(metaclass=IdentityAuthBase): 51 | """ 52 | "Registry for oauth clients. 53 | """ 54 | 55 | def __init__(self, auth_config, framework): 56 | 57 | self.framework = framework 58 | self.auth_config = AuthConfig(auth_config) 59 | self.op_configuration = OPConfiguration( 60 | self.auth_config) 61 | self.credentials = None 62 | self.oidc_flow = OIDCFlow(self.auth_config, 63 | self.op_configuration) 64 | self.token_response: TokenResponse = None 65 | 66 | def prepare_params_for_workflow(self): 67 | 68 | constructor_kwargs = dict( 69 | redirect_uri=self.auth_config["logout_callback_url"], 70 | op_configuration=self.op_configuration, 71 | pkce=self.auth_config["enable_pkce"], 72 | prompt=self.auth_config["prompt"], code_verifier=None) 73 | return constructor_kwargs 74 | 75 | def sign_in(self, request): 76 | """Let the framework do the logic and call the send_sign_in_request 77 | with code The logic should be as follows: 78 | Check whether the session available and ID token available 79 | if yes : 80 | return the user details, id token etc. 81 | else: 82 | return calling the "send_sign_in_request" 83 | 84 | Before calling the send_sign_in_request you must ensure you extracted 85 | the code from the request. Should check whether request has the code. 86 | """ 87 | raise NotImplementedError() 88 | 89 | def send_sign_in_request(self, request, code): 90 | 91 | result = {} 92 | if code: 93 | self.token_response = self.oidc_flow.send_token_request(code) 94 | authenticated_user = self.get_authenticated_user( 95 | self.token_response.decoded_payload) 96 | self.set_post_auth_session_data(user=authenticated_user.get_user(), 97 | username=authenticated_user.username, 98 | access_token=self.token_response.access_token, 99 | id_token=self.token_response.id_token, 100 | id_token_jwt=self.token_response.id_token_jwt) 101 | result[TOKEN_RESPONSE] = self.token_response, authenticated_user 102 | else: 103 | response = self.oidc_flow.send_authorization_request() 104 | self.save_authorize_data(request=request, 105 | redirect_uri=self.auth_config.login_callback_url, 106 | **response) 107 | result[REDIRECT] = response[URL] 108 | return result 109 | 110 | def get_authorization_code(self, request): 111 | """Retrieve parameters for fetching access token, those parameters come 112 | from request and previously saved temporary data in session. 113 | """ 114 | raise NotImplementedError() 115 | 116 | def save_authorize_data(self, request, **kwargs): 117 | """Save temporary data into session for the authorization step. These 118 | data can be retrieved later when fetching access token. 119 | """ 120 | logger.debug('Saving authorize data: {!r}'.format(kwargs)) 121 | keys = [ 122 | 'redirect_uri', 'request_token', 123 | 'state', 'code_verifier', 'nonce' 124 | ] 125 | for k in kwargs: 126 | if k in keys: 127 | self.framework.set_session_data(request, k, kwargs[k]) 128 | 129 | def validate_state_param(self, request, request_state): 130 | 131 | state = self.framework.get_session_data(request, STATE) 132 | if state != request_state: 133 | raise AsgardeoAuthError("CSRF Warning! State not equal in request " 134 | "and response.") 135 | 136 | def get_authenticated_user(self, decoded_payload): 137 | 138 | params = { 139 | "display_name": decoded_payload.get("preferred_username", 140 | decoded_payload.get("sub", 141 | None)), 142 | "email": decoded_payload.get("email", None), 143 | "username": decoded_payload.get("sub", None) 144 | } 145 | 146 | return AuthenticatedUser(**params) 147 | 148 | def send_refresh_token_request(self, refresh_token): 149 | self.oidc_flow.send_refresh_token_request(refresh_token) 150 | 151 | def set_post_auth_session_data(self, **kwargs): 152 | """Save temporary data into session for the User information and the 153 | token. These data can be retrieved later when fetching access token. 154 | """ 155 | for k in kwargs: 156 | if k in post_auth_session_keys: 157 | self.framework.set_session_data(None, k, kwargs[k]) 158 | 159 | def clear_post_auth_session_data(self): 160 | """Save temporary data into session for the User information and the 161 | token. These data can be retrieved later when fetching access token. 162 | """ 163 | for k in post_auth_session_keys: 164 | self.framework.clear_session_data(None, k) 165 | 166 | def get_post_auth_session_data(self): 167 | """Save temporary data into session for the User information and the 168 | token. These data can be retrieved later when fetching access token. 169 | """ 170 | result = {} 171 | for k in post_auth_session_keys: 172 | result[k] = self.framework.get_session_data(None, k) 173 | return result 174 | 175 | def sign_out(self): 176 | """Let the framework do the logic and call the sign_out() 177 | """ 178 | raise NotImplementedError() 179 | 180 | def send_sign_out_request(self): 181 | id_token = self.framework.get_session_data(None, ID_TOKEN_JWT) 182 | if not id_token: 183 | return self.auth_config.login_callback_url 184 | self.clear_post_auth_session_data() 185 | return self.oidc_flow.get_logout_url(id_token=id_token) 186 | -------------------------------------------------------------------------------- /asgardeo_auth/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wso2-attic/asgardeo-auth-python-sdk/87dba49f522a8e0192dcbd5c9a9bb2503a930949/asgardeo_auth/common/__init__.py -------------------------------------------------------------------------------- /asgardeo_auth/common/security.py: -------------------------------------------------------------------------------- 1 | """This module holds the security related functions to facilitate the asgardeo_auth_python_sdk.""" 2 | 3 | import string 4 | import random 5 | 6 | UNICODE_ASCII_CHARACTER_SET = string.ascii_letters + string.digits 7 | 8 | 9 | def generate_token(length=30, chars=UNICODE_ASCII_CHARACTER_SET): 10 | """ 11 | Generate a random token. 12 | 13 | Args: 14 | length : Defaults to 30 15 | chars : Defaults to UNICODE_ASCII_CHARACTER_SET 16 | """ 17 | rand = random.SystemRandom() 18 | return ''.join(rand.choice(chars) for _ in range(length)) 19 | -------------------------------------------------------------------------------- /asgardeo_auth/constants/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wso2-attic/asgardeo-auth-python-sdk/87dba49f522a8e0192dcbd5c9a9bb2503a930949/asgardeo_auth/constants/__init__.py -------------------------------------------------------------------------------- /asgardeo_auth/constants/common.py: -------------------------------------------------------------------------------- 1 | AUTHORIZATION_CODE_TYPE = 'authorization_code' 2 | BASIC_TYPE = 'basic' 3 | 4 | LOGIN_SCOPE = 'internal_login' 5 | HUMAN_TASK_SCOPE = 'internal_humantask_view' 6 | DEFAULT_SUPER_TENANT = 'carbon.super' 7 | ISSUER = 'issuer' 8 | 9 | CODE = 'code' 10 | TRUE_STRING = 'true' 11 | 12 | AUTHORIZATION_ENDPOINT = 'authorization_endpoint' 13 | TOKEN_ENDPOINT = 'token_endpoint' 14 | REVOKE_TOKEN_ENDPOINT = 'revoke_token_endpoint' 15 | END_SESSION_ENDPOINT = 'end_session_endpoint' 16 | JWKS_ENDPOINT = 'jwks_uri' 17 | OP_CONFIG_INITIATED = 'op_config_initiated' 18 | TENANT = 'tenant' 19 | 20 | BASE_URI = 'https://localhost:9443' 21 | AUTH_URI = '/oauth2/authorize' 22 | DEVICE_URI = 'https://oauth2.googleapis.com/device/code' 23 | REVOKE_URI = 'https://oauth2.googleapis.com/revoke' 24 | TOKEN_URI = '/oauth2/token' 25 | TOKEN_INFO_URI = 'https://oauth2.googleapis.com/tokeninfo' 26 | 27 | REDIRECT = 'redirect' 28 | USER = 'user' 29 | TOKEN_RESPONSE = 'token_response' 30 | URL = 'url' 31 | CERTIFICATE_PATH = "certificate_path" 32 | -------------------------------------------------------------------------------- /asgardeo_auth/constants/endpoints.py: -------------------------------------------------------------------------------- 1 | AUTHORIZATION_ENDPOINT = "authorization_endpoint" 2 | TOKEN_ENDPOINT = "token_endpoint" 3 | REVOKE_TOKEN_ENDPOINT = "revocation_endpoint" 4 | END_SESSION_ENDPOINT = "end_session_endpoint" 5 | USER_INFO_ENDPOINT = "userinfo_endpoint" 6 | JWKS_ENDPOINT = "jwks_uri" 7 | OP_CONFIG_INITIATED = "op_config_initiated" 8 | INTROSPECTION_ENDPOINT = "introspection_endpoint" 9 | TENANT = "tenant" 10 | 11 | SERVICE_RESOURCES = { 12 | "authorize": "/oauth2/authorize", 13 | "jwks": "/oauth2/jwks", 14 | "logout": "/oidc/logout", 15 | "revoke": "/oauth2/revoke", 16 | "token": "/oauth2/token", 17 | "introspect": "/oauth2/introspect", 18 | "tenant": "carbon.super", 19 | "well_known": "/oauth2/token/.well-known/openid-configuration" 20 | } 21 | -------------------------------------------------------------------------------- /asgardeo_auth/constants/token.py: -------------------------------------------------------------------------------- 1 | ACCESS_TOKEN = "access_token" 2 | ACCESS_TOKEN_EXPIRE_IN = "expires_in" 3 | ACCESS_TOKEN_ISSUED_AT = "issued_at" 4 | AUTHORIZATION_CODE = "code" 5 | STATE = "state" 6 | ID_TOKEN = "id_token" 7 | ID_TOKEN_JWT = "id_token_jwt" 8 | OIDC_SCOPE = "openid" 9 | PKCE_CODE_VERIFIER = "pkce_code_verifier" 10 | REFRESH_TOKEN = "refresh_token" 11 | SCOPE = "scope" 12 | TOKEN_TYPE = "token_type" 13 | REQUEST_PARAMS = "request_params" 14 | ISSUER = "issuer" 15 | -------------------------------------------------------------------------------- /asgardeo_auth/constants/user.py: -------------------------------------------------------------------------------- 1 | USER_IMAGE = "userimage" 2 | USERNAME = "username" 3 | EMAIL = "email" 4 | DISPLAY_NAME = "display_name" 5 | -------------------------------------------------------------------------------- /asgardeo_auth/exception/__init__.py: -------------------------------------------------------------------------------- 1 | from .asgardeo_auth_error import AsgardeoAuthError 2 | -------------------------------------------------------------------------------- /asgardeo_auth/exception/asgardeo_auth_error.py: -------------------------------------------------------------------------------- 1 | class AsgardeoAuthError(Exception): 2 | """Base error for this module.""" 3 | -------------------------------------------------------------------------------- /asgardeo_auth/framework.py: -------------------------------------------------------------------------------- 1 | class Framework(object): 2 | 3 | def __init__(self, name="wso2_is"): 4 | self.name = name 5 | 6 | def set_session_data(self, request, key, value): 7 | """set the session variable according to the integrated framework""" 8 | raise NotImplementedError() 9 | 10 | def get_session_data(self, request, key): 11 | """get the session variable according to the integrated framework""" 12 | raise NotImplementedError() 13 | 14 | def is_session_data_available(self, request, key): 15 | """check the availability of the session variable according to the 16 | integrated framework """ 17 | raise NotImplementedError() 18 | 19 | def clear_session_data(self, request, key): 20 | """check the availability of the session variable according to the 21 | integrated framework """ 22 | raise NotImplementedError() 23 | -------------------------------------------------------------------------------- /asgardeo_auth/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wso2-attic/asgardeo-auth-python-sdk/87dba49f522a8e0192dcbd5c9a9bb2503a930949/asgardeo_auth/models/__init__.py -------------------------------------------------------------------------------- /asgardeo_auth/models/auth_config.py: -------------------------------------------------------------------------------- 1 | """This module is for holding the authentication related configs.""" 2 | 3 | from ..constants.common import AUTHORIZATION_CODE_TYPE, LOGIN_SCOPE, \ 4 | HUMAN_TASK_SCOPE, DEFAULT_SUPER_TENANT 5 | from ..exception.asgardeo_auth_error import AsgardeoAuthError 6 | 7 | defaultConfig = { 8 | "login_callback_url", 9 | "logout_callback_url", 10 | "client_host", 11 | "authorization_type", 12 | "client_id", 13 | "client_secret", 14 | "consent_denied", 15 | "enable_pkce", 16 | "response_mode", 17 | "scope", 18 | "tenant", 19 | "tenant_path", 20 | "prompt", 21 | "server_origin", 22 | "code_verifier", 23 | "certificate_path" 24 | } 25 | 26 | 27 | class AuthConfig: 28 | """Base class for holding the authentication related configs. 29 | 30 | Store and retrieve a single credential. This class supports locking 31 | such that multiple processes and threads can operate on a single 32 | store. 33 | """ 34 | 35 | def __init__(self, auth_config): 36 | 37 | self.scope_string = None 38 | self.login_callback_url = None 39 | self.logout_callback_url = None 40 | self.client_host = None 41 | self.authorization_type = AUTHORIZATION_CODE_TYPE 42 | self.client_id = None 43 | self.client_secret = None 44 | self.consent_denied = False 45 | self.enable_pkce = False 46 | self.response_mode = None 47 | self.scope = [LOGIN_SCOPE] 48 | self.tenant = DEFAULT_SUPER_TENANT 49 | self.tenant_path = "/t/" + DEFAULT_SUPER_TENANT 50 | self.prompt = "" 51 | self.server_origin = "https://localhost:9443" 52 | self.code_verifier = None 53 | self.certificate_path = None 54 | 55 | for key in auth_config: 56 | if key in defaultConfig: 57 | setattr(self, key, auth_config[key]) 58 | else: 59 | raise AsgardeoAuthError( 60 | "Improper key value passed in the autoconfig. Please check the auth config") 61 | -------------------------------------------------------------------------------- /asgardeo_auth/models/authenticated_user.py: -------------------------------------------------------------------------------- 1 | class AuthenticatedUser: 2 | 3 | def __init__(self, display_name, email, username): 4 | self.display_name: str = display_name 5 | self.email: str = email 6 | self.username: str = username 7 | 8 | def get_user(self): 9 | return self.__dict__ 10 | -------------------------------------------------------------------------------- /asgardeo_auth/models/crypto.py: -------------------------------------------------------------------------------- 1 | from jose import jwt 2 | 3 | 4 | def get_supported_signature_algorithms(): 5 | return ["RS256", "RS512", "RS384", "PS256"] 6 | 7 | 8 | def validate_jwt(id_token, jwks, client_id, issuer): 9 | return jwt.decode(id_token, 10 | jwks, 11 | audience=client_id, 12 | algorithms=get_supported_signature_algorithms(), 13 | issuer=[issuer], 14 | options={"verify_at_hash": False}) 15 | -------------------------------------------------------------------------------- /asgardeo_auth/models/op_Configuration.py: -------------------------------------------------------------------------------- 1 | import http 2 | 3 | import requests 4 | 5 | from ..constants.common import ISSUER 6 | from ..constants.endpoints import AUTHORIZATION_ENDPOINT, TOKEN_ENDPOINT, \ 7 | JWKS_ENDPOINT, REVOKE_TOKEN_ENDPOINT, END_SESSION_ENDPOINT, \ 8 | SERVICE_RESOURCES, INTROSPECTION_ENDPOINT 9 | from ..exception.asgardeo_auth_error import AsgardeoAuthError 10 | 11 | 12 | class OPConfiguration: 13 | 14 | def __init__(self, auth_config): 15 | 16 | self.op_config_initiated = False 17 | serverHost = auth_config.server_origin + auth_config.tenant_path 18 | well_known_url = serverHost + SERVICE_RESOURCES["well_known"] 19 | try: 20 | resp = requests.get(url=well_known_url, 21 | verify=auth_config.certificate_path) 22 | if resp.status_code != http.client.OK: 23 | raise AsgardeoAuthError( 24 | "Failed to load OpenID provider configuration from: " + well_known_url) 25 | 26 | resp_data = resp.json() 27 | self.authorization_endpoint = resp_data[AUTHORIZATION_ENDPOINT] 28 | self.token_endpoint = resp_data[TOKEN_ENDPOINT] 29 | self.end_session_endpoint = resp_data[END_SESSION_ENDPOINT] 30 | self.jwks_uri = resp_data[JWKS_ENDPOINT] 31 | self.revocation_endpoint = resp_data[REVOKE_TOKEN_ENDPOINT] 32 | self.introspection_endpoint = resp_data[INTROSPECTION_ENDPOINT] 33 | self.tenant = auth_config.tenant 34 | self.issuer = resp_data[ISSUER] 35 | self.op_config_initiated = True 36 | 37 | except: 38 | self.authorization_endpoint = serverHost + SERVICE_RESOURCES[ 39 | "authorize"] 40 | self.token_endpoint = serverHost + SERVICE_RESOURCES["token"] 41 | self.end_session_endpoint = serverHost + SERVICE_RESOURCES["logout"] 42 | self.jwks_uri = serverHost + SERVICE_RESOURCES["jwks"] 43 | self.revocation_endpoint = serverHost + SERVICE_RESOURCES["revoke"] 44 | self.introspection_endpoint = serverHost + SERVICE_RESOURCES[ 45 | "introspect"] 46 | self.tenant = SERVICE_RESOURCES["tenant"] 47 | self.issuer = serverHost + SERVICE_RESOURCES["token"] 48 | self.op_config_initiated = True 49 | raise AsgardeoAuthError( 50 | "Initialized OpenID Provider configuration from default " 51 | "configuration. Because failed to access wellknown endpoint: " 52 | "" + well_known_url) 53 | 54 | def reset_op_configuration(self): 55 | 56 | self.authorization_endpoint = None 57 | self.token_endpoint = None 58 | self.end_session_endpoint = None 59 | self.jwks_uri = None 60 | self.revocation_endpoint = None 61 | self.tenant = None 62 | self.issuer = None 63 | self.op_config_initiated = False 64 | 65 | def is_valid_op_config(self, tenant): 66 | 67 | return self.op_config_initiated and tenant == self.tenant 68 | -------------------------------------------------------------------------------- /asgardeo_auth/models/token_response.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from .. import _helpers 4 | from ..exception.asgardeo_auth_error import AsgardeoAuthError 5 | 6 | 7 | class Credentials(object): 8 | """Base class for all Credentials objects. 9 | """ 10 | 11 | 12 | class TokenResponse(Credentials): 13 | """Credentials object for OAuth 2.0. 14 | """ 15 | 16 | def __init__(self, access_token, client_id, client_secret, refresh_token, 17 | token_expiry, token_uri, user_agent, revoke_uri=None, 18 | id_token=None, token_response=None, scopes=None, 19 | introspection_endpoint=None, id_token_jwt=None, 20 | decoded_payload=None): 21 | """Create an instance of OAuth2Credentials. 22 | 23 | This constructor is instantiated by the OIDC_FLOW. 24 | 25 | Args: 26 | access_token: string, access token. 27 | client_id: string, client identifier. 28 | client_secret: string, client secret. 29 | refresh_token: string, refresh token. 30 | token_expiry: datetime, when the access_token expires. 31 | token_uri: string, URI of token endpoint. 32 | user_agent: string, The HTTP User-Agent to provide for this 33 | application. 34 | revoke_uri: string, URI for revoke endpoint. Defaults to None; a 35 | token can't be revoked if this is None. 36 | id_token: object, The identity of the resource owner. 37 | token_response: dict, the decoded response to the token request. 38 | None if a token hasn't been requested yet. Stored 39 | because some providers (e.g. wordpress.com) include 40 | extra fields that clients may want. 41 | scopes: list, authorized scopes for these credentials. 42 | introspection_endpoint: string, the URI for the token info endpoint. 43 | Defaults to None; scopes can not be refreshed if 44 | this is None. 45 | id_token_jwt: string, the encoded and signed identity JWT. The 46 | decoded version of this is stored in id_token. 47 | 48 | Notes: 49 | store: callable, A callable that when passed a Credential 50 | will store the credential back to where it came from. 51 | This is needed to store the latest access_token if it 52 | has expired and been refreshed. 53 | """ 54 | self.access_token = access_token 55 | self.client_id = client_id 56 | self.client_secret = client_secret 57 | self.refresh_token = refresh_token 58 | self.store = None 59 | self.token_expiry = token_expiry 60 | self.token_uri = token_uri 61 | self.user_agent = user_agent 62 | self.revoke_uri = revoke_uri 63 | self.id_token = id_token 64 | self.id_token_jwt = id_token_jwt 65 | self.token_response = token_response 66 | self.scopes = set(_helpers.string_to_scopes(scopes or [])) 67 | self.introspection_endpoint = introspection_endpoint 68 | self.decoded_payload = decoded_payload 69 | 70 | # True if the credentials have been revoked or expired and can't be 71 | # refreshed. 72 | self.invalid = False 73 | 74 | 75 | def extract_id_token(id_token): 76 | """Extract the JSON payload from a JWT. 77 | 78 | Does the extraction w/o checking the signature. 79 | 80 | Args: 81 | id_token: string or bytestring, OAuth 2.0 id_token. 82 | 83 | Returns: 84 | object, The deserialized JSON payload. 85 | """ 86 | if type(id_token) == bytes: 87 | segments = id_token.split(b'.') 88 | else: 89 | segments = id_token.split(u'.') 90 | 91 | if len(segments) != 3: 92 | raise AsgardeoAuthError( 93 | 'Wrong number of segments in token: {0}'.format(id_token)) 94 | 95 | return json.loads( 96 | _helpers._from_bytes(_helpers._urlsafe_b64decode(segments[1]))) 97 | -------------------------------------------------------------------------------- /asgardeo_auth/oidc_flow.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import urllib 3 | from venv import logger 4 | 5 | import requests 6 | import http as http_lib 7 | 8 | from . import pkce, _helpers 9 | from ._helpers import parse_exchange_token_response 10 | from .common.security import generate_token 11 | from .constants.token import OIDC_SCOPE 12 | from .exception.asgardeo_auth_error import AsgardeoAuthError 13 | from .models.auth_config import AuthConfig 14 | from .models.crypto import validate_jwt 15 | from .models.op_Configuration import OPConfiguration 16 | from .models.token_response import TokenResponse, extract_id_token 17 | 18 | _UTCNOW = datetime.datetime.utcnow 19 | 20 | 21 | class Flow(object): 22 | """Base class for all Flow objects.""" 23 | pass 24 | 25 | 26 | class OIDCFlow(Flow): 27 | """This Class does the Web Server Flow . 28 | """ 29 | 30 | def __init__(self, auth_config: AuthConfig = None, 31 | op_configuration: OPConfiguration = None, 32 | user_agent=None, 33 | authorization_header=None, **kwargs): 34 | """Constructor for OIDCFlow. The kwargs argument is used to set extra 35 | query parameters on the For example, the access_type and prompt query 36 | parameters can be set via kwargs. 37 | 38 | :param auth_config: authentication related user configs. 39 | :param op_configuration: openid configuration 40 | :param user_agent: string, HTTP User-Agent to provide for this 41 | application. 42 | :param authorization_header: string, For use with OAuth 2.0 providers 43 | that require a client to authenticate using a header value 44 | instead of passing client_secret in the POST body. 45 | :param kwargs: dict, The keyword arguments are all optional 46 | and required parameters for the OAuth calls. 47 | """ 48 | 49 | # scope is a required argument, but to preserve backwards-compatibility 50 | # we don't want to rearrange the positional arguments 51 | if auth_config.scope is None: 52 | raise TypeError("The value of scope must not be None") 53 | 54 | if auth_config.client_id is None: 55 | raise TypeError("The value of client_id must not be None") 56 | 57 | if auth_config.client_id is None: 58 | raise TypeError("The value of client_id must not be None") 59 | 60 | self.auth_config: AuthConfig = auth_config 61 | self.op_configuration: OPConfiguration = op_configuration 62 | if OIDC_SCOPE not in self.auth_config.scope: 63 | self.auth_config.scope.append(OIDC_SCOPE) 64 | 65 | self.auth_config.scope_string = _helpers.scopes_to_string( 66 | self.auth_config.scope) 67 | self.user_agent = user_agent 68 | self.authorization_header = authorization_header 69 | self.params = _oauth2_web_server_flow_params(kwargs) 70 | 71 | def get_authorize_url(self, login_callback_url=None, state=None): 72 | """Returns a URI to redirect to the provider. 73 | 74 | Args: login_callback_url: string, Either the string 75 | 'urn:ietf:wg:oauth:2.0:oob' for a non-web-based application, or a URI 76 | that handles the callback from the authorization server. This 77 | parameter is deprecated, please move to passing the redirect_uri in 78 | via the constructor. state: string, Opaque state string which is 79 | passed through the OAuth2 flow and returned to the client as a query 80 | parameter in the callback. 81 | 82 | Returns: 83 | A URI as a string to redirect the user to begin the authorization 84 | flow. 85 | """ 86 | if login_callback_url is not None: 87 | logger.warning(( 88 | 'The redirect_uri parameter for ' 89 | 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. ' 90 | 'Please move to passing the redirect_uri in via the ' 91 | 'constructor.')) 92 | self.auth_config.login_callback_url = login_callback_url 93 | 94 | if self.auth_config.login_callback_url is None: 95 | raise ValueError('The value of redirect_uri must not be None.') 96 | 97 | query_params = { 98 | 'client_id': self.auth_config.client_id, 99 | 'redirect_uri': self.auth_config.login_callback_url, 100 | 'scope': self.auth_config.scope_string, 101 | } 102 | if state is None: 103 | state = generate_token() 104 | query_params['state'] = state 105 | 106 | if self.auth_config.enable_pkce: 107 | if not self.auth_config.code_verifier: 108 | self.auth_config.code_verifier = pkce.code_verifier() 109 | challenge = pkce.code_challenge(self.auth_config.code_verifier) 110 | query_params['code_challenge'] = challenge 111 | query_params['code_challenge_method'] = 'S256' 112 | 113 | query_params.update(self.params) 114 | 115 | return _helpers.update_query_params( 116 | self.op_configuration.authorization_endpoint, query_params), state 117 | 118 | def send_token_request(self, code=None): 119 | """Exchanges a code for OAuth2Credentials. 120 | 121 | Args: 122 | code: string, a dict-like object, or None. For a non-device 123 | flow, this is either the response code as a string, or a 124 | dictionary of query parameters to the redirect_uri. For a 125 | device flow, this should be None. 126 | 127 | Returns: 128 | An OAuth2Credentials object that can be used to authorize requests. 129 | 130 | Raises: 131 | FlowExchangeError: if a problem occurred exchanging the code for a 132 | refresh_token. 133 | ValueError: if code and device_flow_info are both provided or both 134 | missing. 135 | """ 136 | if code is None: 137 | raise ValueError('No code provided.') 138 | 139 | if not self.op_configuration.token_endpoint: 140 | raise ValueError('Invalid token endpoint found.') 141 | 142 | post_data = { 143 | 'client_id': self.auth_config.client_id, 144 | 'code': code, 145 | 'scope': self.auth_config.scope_string, 146 | } 147 | if self.auth_config.client_secret is not None: 148 | post_data['client_secret'] = self.auth_config.client_secret 149 | if self.auth_config.enable_pkce: 150 | post_data['code_verifier'] = self.auth_config.code_verifier 151 | else: 152 | post_data['grant_type'] = 'authorization_code' 153 | post_data['redirect_uri'] = self.auth_config.login_callback_url 154 | body = urllib.parse.urlencode(post_data) 155 | 156 | headers = self.get_token_request_headers() 157 | 158 | resp = requests.post(self.op_configuration.token_endpoint, data=body, 159 | headers=headers, 160 | verify=self.auth_config.certificate_path) 161 | content = resp.content 162 | data_response = parse_exchange_token_response(content) 163 | if resp.status_code == http_lib.client.OK and 'access_token' in data_response: 164 | return self.get_token_response_from_resp(data_response) 165 | else: 166 | logger.info('Failed to retrieve access token: %s', content) 167 | if 'error' in data_response: 168 | # you never know what those providers got to say 169 | error_msg = (str(data_response['error']) + 170 | str(data_response.get('error_description', ''))) 171 | else: 172 | error_msg = 'Invalid response: {0}.'.format(str(resp.status)) 173 | raise AsgardeoAuthError(error_msg) 174 | 175 | def send_authorization_request(self): 176 | 177 | url, state = self.get_authorize_url() 178 | result = { 179 | 'url': url, 180 | 'state': state 181 | } 182 | return result 183 | 184 | def get_token_request_headers(self): 185 | 186 | headers = { 187 | 'content-type': 'application/x-www-form-urlencoded', 188 | } 189 | if self.authorization_header is not None: 190 | headers['Authorization'] = self.authorization_header 191 | if self.user_agent is not None: 192 | headers['user-agent'] = self.user_agent 193 | return headers 194 | 195 | def validate_id_token(self, id_token, client_id, issuer): 196 | 197 | jwks_endpoint = self.op_configuration.jwks_uri 198 | if not jwks_endpoint and not len(jwks_endpoint): 199 | raise AsgardeoAuthError("Invalid JWKS URI found.") 200 | resp = requests.get(url=jwks_endpoint, 201 | verify=self.auth_config.certificate_path) 202 | try: 203 | if resp.status_code != http_lib.client.OK: 204 | raise AsgardeoAuthError( 205 | "Failed to load public keys from JWKS URI " + jwks_endpoint) 206 | 207 | keys = resp.json()["keys"] 208 | return validate_jwt(id_token, keys, client_id, issuer) 209 | except AsgardeoAuthError: 210 | raise AsgardeoAuthError("Failed to validate the ID Token") 211 | 212 | def send_refresh_token_request(self, refresh_token): 213 | token_endpoint = self.op_configuration.token_endpoint 214 | if not token_endpoint: 215 | raise AsgardeoAuthError("Invalid token endpoint found.") 216 | 217 | post_data = { 218 | 'client_id': self.auth_config.client_id, 219 | 'refresh_token': refresh_token, 220 | 'grant_type': "refresh_token", 221 | } 222 | 223 | headers = self.get_token_request_headers() 224 | 225 | resp = requests.post(token_endpoint, data=post_data, headers=headers, 226 | verify=self.auth_config.certificate_path) 227 | content = resp.content 228 | data_response = parse_exchange_token_response(content) 229 | if resp.status_code != http_lib.client.OK and 'access_token' in data_response: 230 | raise AsgardeoAuthError( 231 | "Invalid status code received in the refresh token response: " + str( 232 | resp.status_code)) 233 | return self.get_token_response_from_resp(data_response) 234 | 235 | def get_token_response_from_resp(self, resp): 236 | access_token = resp['access_token'] 237 | refresh_token = resp.get('refresh_token', None) 238 | if not refresh_token: 239 | logger.info( 240 | 'Received token response with no refresh_token. Consider ' 241 | "reauthenticating with prompt='consent'.") 242 | token_expiry = None 243 | if 'expires_in' in resp: 244 | delta = datetime.timedelta(seconds=int(resp['expires_in'])) 245 | token_expiry = delta + _UTCNOW() 246 | 247 | extracted_id_token = None 248 | id_token_jwt = None 249 | if 'id_token' in resp: 250 | extracted_id_token = extract_id_token(resp['id_token']) 251 | id_token_jwt = resp['id_token'] 252 | 253 | logger.info('Successfully retrieved access token') 254 | decoded_payload = self.validate_id_token(id_token_jwt, 255 | self.auth_config.client_id, 256 | self.op_configuration.issuer) 257 | return TokenResponse( 258 | access_token, self.auth_config.client_id, 259 | self.auth_config.client_secret, 260 | refresh_token, token_expiry, self.op_configuration.token_endpoint, 261 | self.user_agent, 262 | revoke_uri=self.op_configuration.revocation_endpoint, 263 | id_token=extracted_id_token, 264 | id_token_jwt=id_token_jwt, token_response=resp, 265 | scopes=self.auth_config.scope_string, 266 | introspection_endpoint=self.op_configuration.introspection_endpoint, 267 | decoded_payload=decoded_payload) 268 | 269 | def send_revoke_token_request(self, access_token): 270 | 271 | revocation_endpoint = self.op_configuration.revocation_endpoint 272 | 273 | if not revocation_endpoint or not len(revocation_endpoint.strip()): 274 | raise AsgardeoAuthError("Invalid revoke token endpoint found.") 275 | 276 | post_data = { 277 | 'client_id': self.auth_config.client_id, 278 | 'token': access_token, 279 | 'token_type_hint': "access_token", 280 | } 281 | 282 | headers = self.get_token_request_headers() 283 | 284 | resp = requests.post(revocation_endpoint, data=post_data, 285 | headers=headers, 286 | verify=self.auth_config.certificate_path) 287 | if resp.status_code != http_lib.client.OK: 288 | raise AsgardeoAuthError( 289 | "Invalid status code received in the revoke token response: " + str( 290 | resp.status_code)) 291 | return resp.json() 292 | 293 | def get_logout_url(self, logout_callback_url=None, id_token=None): 294 | """Returns a URI to redirect to the provider. 295 | 296 | Args: login_callback_url: string, Either the string 297 | 'urn:ietf:wg:oauth:2.0:oob' for a non-web-based application, or a URI 298 | that handles the callback from the authorization server. This 299 | parameter is deprecated, please move to passing the redirect_uri in 300 | via the constructor. state: string, Opaque state string which is 301 | passed through the OAuth2 flow and returned to the client as a query 302 | parameter in the callback. 303 | 304 | Returns: 305 | A URI as a string to redirect the user to begin the authorization 306 | flow. 307 | """ 308 | 309 | logout_endpoint = self.op_configuration.end_session_endpoint 310 | 311 | if not logout_endpoint: 312 | raise AsgardeoAuthError("No logout endpoint found in the session.") 313 | 314 | if not id_token: 315 | raise AsgardeoAuthError("Invalid id_token found in the session.") 316 | 317 | if logout_callback_url is not None: 318 | logger.warning(( 319 | 'The redirect_uri parameter for ' 320 | 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. ' 321 | 'Please move to passing the redirect_uri in via the ' 322 | 'constructor.')) 323 | self.auth_config.logout_callback_url = logout_callback_url 324 | 325 | if not self.auth_config.logout_callback_url: 326 | raise ValueError('The value of redirect_uri must not be None.') 327 | 328 | query_params = { 329 | 'post_logout_redirect_uri': self.auth_config.logout_callback_url, 330 | 'id_token_hint': id_token 331 | } 332 | 333 | return _helpers.update_query_params(logout_endpoint, query_params) 334 | 335 | 336 | def _oauth2_web_server_flow_params(kwargs): 337 | """Configures redirect URI parameters for OAuth2WebServerFlow.""" 338 | params = { 339 | 'access_type': 'offline', 340 | 'response_type': 'code', 341 | } 342 | 343 | params.update(kwargs) 344 | 345 | # Check for the presence of the deprecated approval_prompt param and 346 | # warn appropriately. 347 | approval_prompt = params.get('approval_prompt') 348 | if approval_prompt is not None: 349 | logger.warning( 350 | 'The approval_prompt parameter for OAuth2WebServerFlow is ' 351 | 'deprecated. Please use the prompt parameter instead.') 352 | 353 | if approval_prompt == 'force': 354 | logger.warning( 355 | 'approval_prompt="force" has been adjusted to ' 356 | 'prompt="consent"') 357 | params['prompt'] = 'consent' 358 | del params['approval_prompt'] 359 | 360 | return params 361 | -------------------------------------------------------------------------------- /asgardeo_auth/pkce.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import hashlib 3 | import os 4 | 5 | 6 | def code_verifier(n_bytes=64): 7 | """ 8 | Generates a 'code_verifier' as described in section 4.1 of RFC 7636. 9 | 10 | This is a 'high-entropy cryptographic random string' that will be 11 | impractical for an attacker to guess. 12 | 13 | Args: 14 | n_bytes: integer between 31 and 96, inclusive. default: 64 15 | number of bytes of entropy to include in verifier. 16 | 17 | Returns: 18 | Bytestring, representing urlsafe base64-encoded random data. 19 | """ 20 | verifier = base64.urlsafe_b64encode(os.urandom(n_bytes)).rstrip(b'=') 21 | if len(verifier) < 43: 22 | raise ValueError("Verifier too short. n_bytes must be > 30.") 23 | elif len(verifier) > 128: 24 | raise ValueError("Verifier too long. n_bytes must be < 97.") 25 | else: 26 | return verifier 27 | 28 | 29 | def code_challenge(verifier): 30 | """ 31 | Creates a 'code_challenge' as described in section 4.2 of RFC 7636 32 | by taking the sha256 hash of the verifier and then urlsafe 33 | base64-encoding it. 34 | 35 | Args: 36 | verifier: bytestring, representing a code_verifier as generated by 37 | code_verifier(). 38 | 39 | Returns: 40 | Bytestring, representing a urlsafe base64-encoded sha256 hash digest, 41 | without '=' padding. 42 | """ 43 | digest = hashlib.sha256(verifier).digest() 44 | return base64.urlsafe_b64encode(digest).rstrip(b'=') 45 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-jose==3.2.0 2 | Flask==1.1.2 3 | six 4 | requests 5 | pyOpenSSL>=20.0.1 -------------------------------------------------------------------------------- /samples/flask/Readme.md: -------------------------------------------------------------------------------- 1 | # Asgardeo - OIDC Flask SDK Usage Example 2 | --- 3 | 4 | ### Register an Application 5 | 6 | Run Developer Portal and register a Web Application with minimal configuration. 7 | Give `https://localhost:3000/login` as the callback URL. 8 | 9 | ### Setup and run sample 10 | 11 | 1. Update your configurations in `conf.py` with Asgardeo App Register details. 12 | 13 | E.g. 14 | 15 | ```python 16 | auth_config = { 17 | "login_callback_url": "https://localhost:3000/login", 18 | "logout_callback_url": "https://localhost:3000/signin", 19 | "client_host": "https://localhost:3000", 20 | "client_id": "", 21 | "client_secret": "", 22 | "server_origin": "https://api.asgardeo.io", 23 | "tenant_path": "/t/", 24 | "tenant": "", 25 | "certificate_path": "cert/wso2.crt" 26 | } 27 | ``` 28 | 29 | 2. Initialize the sdk 30 | ```python 31 | identity_auth = FlaskIdentityAuth(auth_config=auth_config) 32 | ``` 33 | 34 | 35 | 3. Add signin implementation 36 | ```python 37 | @app.route("/login") 38 | def login(): 39 | response = identity_auth.sign_in() 40 | if REDIRECT in response: 41 | return redirect(response[REDIRECT]) 42 | elif TOKEN_RESPONSE in response: 43 | credentials, authenticated_user = response[TOKEN_RESPONSE] 44 | return redirect(url_for('home')) 45 | else: 46 | raise IdentityAuthError("Error occurred on the sign in Process Please Try again later") 47 | ``` 48 | 49 | 4. Add signout implementation 50 | ```python 51 | @app.route('/logout') 52 | def logout(): 53 | return identity_auth.sign_out() 54 | ``` 55 | 56 | 5. Navigate to `https://localhost:5000` from the browser 57 | -------------------------------------------------------------------------------- /samples/flask/app.py: -------------------------------------------------------------------------------- 1 | """ Sample Flask based application for Asgardeo OIDC SDK. 2 | 3 | This application demonstrates Asgardeo OIDC SDK capabilities. 4 | 5 | """ 6 | 7 | from functools import wraps 8 | from http.client import HTTPException 9 | 10 | from flask import Flask, redirect, jsonify, url_for, render_template 11 | 12 | from conf import auth_config 13 | from asgardeo_auth.Integration.flask_client import FlaskAsgardeoAuth 14 | from asgardeo_auth.exception.asgardeo_auth_error import \ 15 | AsgardeoAuthError 16 | from constants import REDIRECT, TOKEN_RESPONSE, USERNAME 17 | 18 | app = Flask(__name__) 19 | app.secret_key = 'super_secret_key' 20 | 21 | # initialize the app 22 | identity_auth = FlaskAsgardeoAuth(auth_config=auth_config) 23 | 24 | 25 | def requires_auth(f): 26 | """ 27 | Decorator to secure the protected endpoint which require user 28 | authentication. 29 | 30 | Args: 31 | f : function to be decorated 32 | """ 33 | 34 | @wraps(f) 35 | def decorated(*args, **kwargs): 36 | """ 37 | Decorator to redirect user to the dashboard. 38 | """ 39 | if not identity_auth.is_session_data_available(USERNAME): 40 | return redirect(url_for('dashboard')) 41 | return f(*args, **kwargs) 42 | 43 | return decorated 44 | 45 | 46 | @app.errorhandler(Exception) 47 | def handle_auth_error(ex): 48 | """ 49 | Handle an authentication error. 50 | 51 | Args: 52 | ex : Exception to handle. 53 | """ 54 | response = jsonify(message=str(ex)) 55 | response.status_code = (ex.code if isinstance(ex, HTTPException) else 500) 56 | return response 57 | 58 | 59 | @app.route('/') 60 | @requires_auth 61 | def home(): 62 | """ 63 | Render the login page. 64 | """ 65 | session_data = identity_auth.get_post_auth_session_data() 66 | return render_template('/dashboard.html', session_data=session_data) 67 | 68 | 69 | @app.route('/signin') 70 | def dashboard(): 71 | """ 72 | Render the dashboard page. 73 | """ 74 | return render_template('/index.html') 75 | 76 | 77 | @app.route('/login') 78 | def login(): 79 | """ 80 | Login to implementation from asgardeo_auth_python_sdk. 81 | """ 82 | response = identity_auth.sign_in() 83 | if REDIRECT in response: 84 | return redirect(response[REDIRECT]) 85 | elif TOKEN_RESPONSE in response: 86 | credentials, authenticated_user = response[TOKEN_RESPONSE] 87 | return redirect(url_for('home')) 88 | else: 89 | raise AsgardeoAuthError( 90 | 'Error occurred on the sign in Process Please Try again later') 91 | 92 | 93 | @app.route('/logout') 94 | def logout(): 95 | """ 96 | Logout implementation from asgardeo_auth_python_sdk. 97 | """ 98 | return identity_auth.sign_out() 99 | 100 | 101 | if __name__ == '__main__': 102 | app.debug = True 103 | app.run(host='localhost', port=3000, ssl_context='adhoc') 104 | -------------------------------------------------------------------------------- /samples/flask/cert/wso2.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDqTCCApGgAwIBAgIEXbABozANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJV 3 | UzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxDTALBgNVBAoM 4 | BFdTTzIxDTALBgNVBAsMBFdTTzIxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xOTEw 5 | MjMwNzMwNDNaFw0yMjAxMjUwNzMwNDNaMGQxCzAJBgNVBAYTAlVTMQswCQYDVQQI 6 | DAJDQTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzENMAsGA1UECgwEV1NPMjENMAsG 7 | A1UECwwEV1NPMjESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF 8 | AAOCAQ8AMIIBCgKCAQEAxeqoZYbQ/Sr8DOFQ+/qbEbCp6Vzb5hzH7oa3hf2FZxRK 9 | F0H6b8COMzz8+0mvEdYVvb/31jMEL2CIQhkQRol1IruD6nBOmkjuXJSBficklMaJ 10 | ZORhuCrB4roHxzoG19aWmscA0gnfBKo2oGXSjJmnZxIh+2X6syHCfyMZZ00LzDyr 11 | goXWQXyFvCA2ax54s7sKiHOM3P4A9W4QUwmoEi4HQmPgJjIM4eGVPh0GtIANN+BO 12 | Q1KkUI7OzteHCTLu3VjxM0sw8QRayZdhniPF+U9n3fa1mO4KLBsW4mDLjg8R/JuA 13 | GTX/SEEGj0B5HWQAP6myxKFz2xwDaCGvT+rdvkktOwIDAQABo2MwYTAUBgNVHREE 14 | DTALgglsb2NhbGhvc3QwHQYDVR0OBBYEFEDpLB4PDgzsdxD2FV3rVnOr/A0DMB0G 15 | A1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjALBgNVHQ8EBAMCBPAwDQYJKoZI 16 | hvcNAQELBQADggEBAE8H/axAgXjt93HGCYGumULW2lKkgqEvXryP2QkRpbyQSsTY 17 | cL7ZLSVB7MVVHtIsHh8f1C4Xq6Qu8NUrqu5ZLC1pUByaqR2ZIzcj/OWLGYRjSTHS 18 | VmVIq9QqBq1j7r6f3BWqaOIiknmTzEuqIVlOTY0gO+SHdS62vr2FCz4yOrBEulGA 19 | vomsU8sqg4PhFnkhxI4M912Ly+2RgN9L7AkhzK+EzXY1/QtlI/VysNfS6zrHasKz 20 | 6CrKKCGqQnBnSvSTyF9OR5KFHnkAwE995IZrcSQicMxsLhTMUHDLQ/gRyy7V/ZpD 21 | MfAWR+5OeQiNAp/bG4fjJoTdoqkul51+2bHHVrU= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /samples/flask/conf.py: -------------------------------------------------------------------------------- 1 | """This module keeps the minimal required configuration sample to initialize 2 | the client. """ 3 | 4 | auth_config = { 5 | "login_callback_url": "https://localhost:3000/login", 6 | "logout_callback_url": "https://localhost:3000/signin", 7 | "client_host": "https://localhost:3000", 8 | "client_id": "", 9 | "client_secret": "", 10 | "server_origin": "https://api.asgardeo.io", 11 | "tenant_path": "/t/", 12 | "tenant": "", 13 | "certificate_path": "cert/wso2.crt" 14 | } 15 | -------------------------------------------------------------------------------- /samples/flask/constants.py: -------------------------------------------------------------------------------- 1 | REDIRECT = 'redirect' 2 | TOKEN_RESPONSE = 'token_response' 3 | USERNAME = "username" 4 | -------------------------------------------------------------------------------- /samples/flask/requirements.txt: -------------------------------------------------------------------------------- 1 | asgardeo-auth-python-sdk -------------------------------------------------------------------------------- /samples/flask/static/css/theme.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2021, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. 3 | * 4 | * WSO2 Inc. licenses this file to you under the Apache License, 5 | * Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | padding: 0; 22 | text-align: center; 23 | color: #2A2A2A; 24 | background-color: #fff; 25 | font-family: BlinkMacSystemFont, Segoe WPC,Segoe UI, HelveticaNeue-Light, Ubuntu,Droid Sans, sans-serif, 'Helvetica Neue', Arial, Helvetica,sans-serif; 26 | } 27 | 28 | h1, h2, h3, h4, h5 { 29 | font-weight: 300; 30 | } 31 | 32 | h1 b, h2 b, h3 b, h4 b, h5 b { 33 | font-weight: 400; 34 | } 35 | 36 | b { 37 | font-weight: 500; 38 | } 39 | 40 | a { 41 | color: #f47421; 42 | } 43 | 44 | a:hover { 45 | text-decoration: none;; 46 | } 47 | 48 | .mb-0 { 49 | margin-bottom: 0 !important; 50 | } 51 | 52 | .mt-4 { 53 | margin-top: 40px !important; 54 | } 55 | 56 | .home-image { 57 | vertical-align: middle; 58 | margin-bottom: 50px; 59 | } 60 | 61 | .home-image .logo { 62 | width: 150px; 63 | vertical-align: middle; 64 | } 65 | 66 | .home-image .logo-plus { 67 | display: inline-block; 68 | font-size: 50px; 69 | margin: 0 30px 0 10px; 70 | vertical-align: middle; 71 | } 72 | 73 | img.footer-image { 74 | width: 50px; 75 | margin: 30px auto; 76 | opacity: .8; 77 | } 78 | 79 | img.logo-image { 80 | width: 200px; 81 | margin-top: 100px; 82 | } 83 | 84 | code { 85 | background: #272822; 86 | padding: 4px; 87 | font-size: 90%; 88 | color: #fd971f; 89 | } 90 | 91 | .code { 92 | background: #272822; 93 | display: block; 94 | word-break: break-all; 95 | padding: 20px; 96 | text-align: left; 97 | } 98 | 99 | .code .id-token-0 { 100 | color: #cc6633; 101 | } 102 | 103 | .code .id-token-1 { 104 | color: #f9f8f5; 105 | } 106 | 107 | .code .id-token-2 { 108 | color: #fd971f; 109 | } 110 | 111 | .row { 112 | width: 100%; 113 | display: flex; 114 | text-align: left; 115 | } 116 | 117 | .row > .column { 118 | flex: 40%; 119 | margin-right: 15px; 120 | } 121 | 122 | .row > .column:last-child { 123 | flex: 60%; 124 | margin-right: 0; 125 | } 126 | 127 | @media screen and (max-width: 600px) { 128 | .column { 129 | width: 100%; 130 | margin-right: 0; 131 | } 132 | } 133 | 134 | .container { 135 | background: #fff; 136 | border: 1px solid #e2e2e2; 137 | width: 980px; 138 | margin: 0 auto; 139 | margin-top: 50px; 140 | border-radius: 10px; 141 | } 142 | 143 | .json { 144 | text-align: left; 145 | display: block; 146 | overflow: auto; 147 | word-break: break-all; 148 | } 149 | 150 | .json .pretty-json-container.object-container { 151 | padding: 20px; 152 | } 153 | 154 | .header-title { 155 | background-color: #f47421; 156 | color: #ffffff; 157 | padding: 20px; 158 | border-radius: 10px 10px 0 0; 159 | } 160 | 161 | .content { 162 | padding: 50px; 163 | } 164 | 165 | .btn.primary, 166 | .btn.primary:active, 167 | .btn.primary:focus { 168 | background-color: #282c34; 169 | border-radius: 3px; 170 | border: none; 171 | color: #ffffff; 172 | display: inline-block; 173 | font-size: 1.14285714rem; 174 | text-align: center; 175 | text-decoration: none; 176 | width: 150px; 177 | cursor: pointer; 178 | -webkit-text-size-adjust: none; 179 | padding: .78571429em 1.5em .78571429em; 180 | margin-top: 30px; 181 | outline: none; 182 | } 183 | 184 | table { 185 | border-collapse: collapse; 186 | width: 100%; 187 | } 188 | 189 | td, th { 190 | border: 1px solid #dddddd; 191 | text-align: left; 192 | padding: 8px; 193 | } 194 | 195 | tr:nth-child(even) { 196 | background-color: #dddddd; 197 | } 198 | -------------------------------------------------------------------------------- /samples/flask/static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wso2-attic/asgardeo-auth-python-sdk/87dba49f522a8e0192dcbd5c9a9bb2503a930949/samples/flask/static/images/favicon.ico -------------------------------------------------------------------------------- /samples/flask/static/images/flask.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /samples/flask/static/images/footer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wso2-attic/asgardeo-auth-python-sdk/87dba49f522a8e0192dcbd5c9a9bb2503a930949/samples/flask/static/images/footer.png -------------------------------------------------------------------------------- /samples/flask/static/images/logo-dark.svg: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 14 | 15 | 17 | 19 | 21 | 23 | 25 | 26 | 27 | 29 | 30 | -------------------------------------------------------------------------------- /samples/flask/static/images/oidc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wso2-attic/asgardeo-auth-python-sdk/87dba49f522a8e0192dcbd5c9a9bb2503a930949/samples/flask/static/images/oidc.png -------------------------------------------------------------------------------- /samples/flask/templates/dashboard.html: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 22 | Home 23 | 24 | 27 | 29 | 30 | 31 | 32 |
33 |
34 | 36 |
37 |
38 |
39 |

40 | Python Flask-Based OIDC Authentication Sample
(OIDC 41 | - Authorization Code Grant) 42 |

43 |
44 |
45 |

46 | Hi {{ session_data.username }} 47 |

48 |

Available user attributes

49 | {% if session_data.user %} 50 | 51 | 52 | 53 | 54 | 55 | {% for key, value in session_data.user.items() %} 56 | {% if value is not none %} 57 | 58 | 59 | 60 | 61 | {% endif %} 62 | {% endfor %} 63 | {% if session_data.access_token %} 64 | 65 | 66 | 67 | 68 | {% endif %} 69 |
User attribute nameValue
{{key}}{{value}}
access_token{{ session_data.access_token }}
70 | {% else %} 71 |

There are no user attributes selected to the application at the 72 | moment.

73 | {% endif %} 74 |
75 |
76 |
77 | 78 |
79 |
80 |
81 |
82 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /samples/flask/templates/index.html: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 22 | 23 | OIDC Sample App 24 | 25 | 27 | 28 | 29 |
30 |
31 | 32 |
33 |
34 |
35 |

36 | Python Flask-Based OIDC Authentication Sample
37 | (OIDC - Authorization Code Grant) 38 |

39 |
40 |
41 |
42 | 44 | + 45 | 47 |
48 |

49 | Sample demo to showcase how to authenticate a simple Flask 50 | application using
51 | Asgardeo with the Asgardeo Python SDK 53 |

54 |
55 | Login 56 |
57 |
58 |
59 | 60 |
61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | author = Asgardeo 3 | author_email = beta@asgardeo.io 4 | license_file = LICENSE 5 | description = Asgardeo Auth Python SDK. 6 | description-file = README.md 7 | classifiers = 8 | Development Status :: 3 - Alpha 9 | Environment :: Console 10 | Environment :: Web Environment 11 | Framework :: Flask 12 | Framework :: Django 13 | Intended Audience :: Developers 14 | License :: OSI Approved :: Apache-2.0 15 | Operating System :: OS Independent 16 | Programming Language :: Python 17 | Programming Language :: Python :: 3 18 | Programming Language :: Python :: 3.6 19 | Programming Language :: Python :: 3.7 20 | Programming Language :: Python :: 3.8 21 | Programming Language :: Python :: 3.9 22 | Topic :: Internet :: WWW/HTTP :: Dynamic Content 23 | Topic :: Internet :: WWW/HTTP :: WSGI :: Application 24 | 25 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | from setuptools import find_packages 3 | 4 | with open('README.md') as f: 5 | long_description = f.read() 6 | 7 | 8 | name = 'asgardeo-auth-python-sdk' 9 | packages = ('asgardeo_auth', 'asgardeo_auth.*') 10 | version = "0.1.14-dev0" 11 | author = 'Asgardeo' 12 | homepage = 'https://github.com/asgardeo/asgardeo-auth-python-sdk#readme' 13 | license_name = 'Apache Software License' 14 | description = "Asgardeo Auth Python SDK." 15 | bug_tracker = 'https://github.com/asgardeo/asgardeo-auth-python-sdk/issues' 16 | keywords = [ 17 | "Asgardeo", 18 | "OIDC", 19 | "OAuth2", 20 | "Authentication", 21 | "Authorization" 22 | ] 23 | download_url = 'https://github.com/asgardeo/asgardeo-auth-python-sdk/releases' 24 | author = "Asgardeo", 25 | author_email = "beta@asgardeo.io" 26 | default_user_agent = '{}/{} (+{})'.format(name, version, homepage) 27 | default_json_headers = [ 28 | ('Content-Type', 'application/json'), 29 | ('Cache-Control', 'no-store'), 30 | ('Pragma', 'no-cache'), 31 | ] 32 | 33 | setup( 34 | name=name, 35 | packages=find_packages(include=packages), 36 | version=version, 37 | license=license_name, 38 | description=description, 39 | long_description=long_description, 40 | long_description_content_type='text/markdown', 41 | author=author, 42 | author_email=author_email, 43 | url=homepage, 44 | download_url=download_url, 45 | keywords=keywords, 46 | install_requires=[ 47 | 'six', 48 | 'requests', 49 | 'python-jose>=3.2.0', 50 | 'Flask>=1.1.2', 51 | 'pyOpenSSL>=20.0.1' 52 | ], 53 | classifiers=[ 54 | 'Development Status :: 3 - Alpha', 55 | # Chose either "3 - Alpha", "4 - Beta" or "5 - Production/Stable" 56 | # as the current state of your package 57 | 'Environment :: Web Environment', 58 | 'Framework :: Flask', 59 | 'Intended Audience :: Developers', 60 | 'Topic :: Software Development :: Build Tools', 61 | 'License :: OSI Approved :: ' + license_name, 62 | 'Operating System :: OS Independent', 63 | 'Programming Language :: Python :: 3', 64 | 'Programming Language :: Python :: 3.4', 65 | 'Programming Language :: Python :: 3.5', 66 | 'Programming Language :: Python :: 3.6', 67 | 'Programming Language :: Python :: 3.7', 68 | 'Programming Language :: Python :: 3.8', 69 | 'Programming Language :: Python :: 3.9' 70 | ], 71 | project_urls={ 72 | 'Documentation': homepage, 73 | 'Bug Tracker': bug_tracker, 74 | 'Source Code': homepage, 75 | } 76 | ) 77 | --------------------------------------------------------------------------------