├── .gitignore ├── README.md └── src ├── op ├── op │ ├── __init__.py │ ├── certs │ │ ├── localhost.crt │ │ └── localhost.key │ ├── config.py.example │ ├── keys │ │ ├── cert.pem │ │ └── key.pem │ └── op.py ├── requirements.txt └── templates │ ├── consent.mako │ ├── list_access_tokens.mako │ └── login.mako ├── pam_module └── pam_oidc_authz.c ├── service_provider ├── __init__.py ├── certs │ ├── server.crt │ └── server.key ├── conf.py.example ├── database.py ├── htdocs │ ├── access_token.mako │ ├── opchoice.mako │ ├── operror.mako │ └── rp_session_iframe.mako ├── oidc.py ├── requirements.txt ├── service_provider.py └── static │ ├── bootstrap │ ├── css │ │ ├── bootstrap-theme.css │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap.css │ │ └── bootstrap.min.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff │ └── js │ │ ├── bootstrap.js │ │ └── bootstrap.min.js │ ├── jquery.min.1.9.1.js │ ├── robots.txt │ └── style.css ├── test_app └── test_app.c └── test_application_login_service ├── login_server.py └── templates └── login_page.mako /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.pyc 3 | *config.py 4 | *conf.py 5 | client_db.db 6 | *jwks.json 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Implementation of OpenID Connect Access Tokens as per client passwords. 2 | 3 | This repository contains a proof-of-concept implementation of 4 | https://tools.ietf.org/html/draft-sakimura-oidc-extension-nonweb-01. 5 | 6 | 7 | ## OpenID Connect Provider 8 | The implementation of a provider with support for non-web access tokens can 9 | be found in `src/op`. It has the following features: 10 | 11 | * Support for "auth scope values" (request with scope value starting with 12 | "auth_" will in long-lived access tokens). 13 | * The end user can name access tokens as part of the authentication flow. 14 | * An authenticated user can revoke access tokens (see below). 15 | * Support for dynamic registration and provider configuration information. 16 | 17 | ### Setup 18 | 1. Install dependencies: 19 | pip install -r src/op/requirements.txt 20 | 1. Configure the provider: 21 | 1. Copy the file `src/op/op/config.py.example` to 22 | `src/op/op/config.py` 23 | 1. Update the base url and paths to SSL certs, encryption/signing keys, etc. 24 | 1. Start the provider: 25 | python op.py -p config 26 | 27 | ### Revoking access tokens 28 | A list of all valid access tokens can be viewed at the endpoint `/my_tokens` 29 | of the provider. From that page each individual access token can be revoked. 30 | 31 | 32 | ## OpenID Connect Relying Party 33 | The implementation of a relying party (RP) can be found in 34 | `src/service_provider`. This RP registers with the provider dynamically and uses 35 | the "Authorization Code Flow" and makes a token request to obtain the access 36 | token. 37 | 38 | ### Setup 39 | 1. Install dependencies: 40 | pip install -r src/service_provider/requirements.txt 41 | 1. Configure the client: 42 | 1. Copy the file `src/service_provider/conf.py.example` to 43 | `src/service_provider/conf.py` 44 | 1. Update the paths to SSL certs and the port if necessary. 45 | 1. Update the `"srv_discovery_url"` of the `"non-web-op"` in `CLIENTS` to 46 | point to the base url of the OpenID Connect Provider described above. 47 | 1. Start the service_provider 48 | python service_provider.py conf 49 | 50 | ## PAM module 51 | 52 | A PAM module to verify an access token can be found in `src/pam_module`. 53 | It sends (using `libcurl`) the access token to the RP 54 | (described above) for authorization. 55 | 56 | ### Setup in Ubuntu 57 | In this section it is described how to set up the PAM module 58 | 59 | #### Install dependencies 60 | apt-get install libpam0g-dev libcurl4-openssl-dev 61 | #### Compile 62 | gcc -fPIC -fno-stack-protector -c pam_oidc_authz.c 63 | #### Create shared library for PAM 64 | mkdir /lib/security 65 | ld -x --shared -o /lib/security/pam_oidc_authz.so pam_oidc_authz.o `curl-config --libs` 66 | 67 | 68 | ## Login server 69 | The login server (located in `src/test_application_login_service`) can be used to 70 | simulate a login service which should be put in front of the RP to ensure only 71 | authorized users can fetch an access token. 72 | 73 | For simplicity this login service does NOT authenticate users, it allows any 74 | user to select any username before entering the RP. 75 | 76 | ### Setup 77 | To start the login server, run the python script 78 | `src/test_application_login_service/login_server.py`. 79 | 80 | The script requires one parameter: the base url to the RP. 81 | The login service will use a HTTP GET request in order to send the username of 82 | the user to the RP. 83 | 84 | It is possible to specify which port the login server should use, by using the 85 | flag `-p` (if it is not specified it defaults to port 80). 86 | 87 | Example: 88 | 89 | python login_server.py -p 8000 https://localhost:8666/ 90 | 91 | 92 | ## Test application 93 | A small application for testing the flow can be found in `src/test_app`. 94 | 95 | ### PAM configuration in Ubuntu 96 | Add the following to the file `/etc/pam.d/test_app`: 97 | 98 | auth requisite pam_oidc_authz.so 99 | account sufficient pam_permit.so 100 | 101 | : 102 | This should be the url to the access token verification endpoint e.g. https://localhost:8666/verify_access_token 103 | 104 | ### Setup 105 | How to use the test application in order test the PAM module. 106 | 107 | #### Compile 108 | gcc -o test_app test_app.c -lpam -lpam_misc 109 | #### Run 110 | ./test_app 111 | 112 | 113 | ## Testing it all 114 | 115 | ### Docker setup 116 | The OP, RP and login server can be automatically started in a Docker container. 117 | See the Dockerfile, with an example Docker Compose file 118 | https://github.com/its-dirg/docker/tree/master/non-web-oidc. 119 | 120 | After building the image, start a Docker container: 121 | 122 | docker run -p 8080:8080 -p 8090:8090 -p 9000:9000 -e HOST_IP= 123 | 124 | The exposed ports are 8080 for the OP, 8090 for the RP and 9000 for the 125 | example login server. And the environment variable `HOST_IP` must be the IP 126 | address for the host computer (outside Docker). 127 | 128 | To test the flow and get an access token, browse to http://:9000 and 129 | follow the guide below from step 4. 130 | 131 | ### Manual setup 132 | 1. Start the provider. 133 | 2. Start the RP. 134 | 3. Start the login server and browse to it. 135 | 4. Enter a username and submit. 136 | 5. Click the blue button "Start" at the RP to use the default non-web provider. 137 | 6. Login at the provider using any of user-password pairs defined in `PASSWD` 138 | in `src/op/op/conf.py`, e.g. user "upper" with password "crust". 139 | 7. Enter a new nickname for the access token, e.g. "test1". 140 | 8. Copy the displayed access token to clipboard. 141 | 9. Paste the access token in a file. 142 | 10. Run the test application passing the username selected in step 4. and 143 | filename created in step 9. 144 | 11. If the access token is valid, the test application will print 145 | "Authenticated" as the last line of output. 146 | 12. Try revoking the token by browsing to the `/my_tokens` endpoint at the 147 | provider. 148 | 13. Run the test application again with the same access token in the file. 149 | 14. If the token was properly revoked the test application will print 150 | "Not Authenticated" 151 | -------------------------------------------------------------------------------- /src/op/op/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'regu0004' 2 | -------------------------------------------------------------------------------- /src/op/op/certs/localhost.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICGTCCAYICCQCIs8fF+AnxUzANBgkqhkiG9w0BAQUFADBRMQswCQYDVQQGEwJT 3 | RTETMBEGA1UECBMKU29tZS1TdGF0ZTEMMAoGA1UEChMDVW1VMQwwCgYDVQQLEwNJ 4 | VFMxETAPBgNVBAMTCHJvaGUwMDAyMB4XDTExMDkwNzA4MjMwOFoXDTEyMDkwNjA4 5 | MjMwOFowUTELMAkGA1UEBhMCU0UxEzARBgNVBAgTClNvbWUtU3RhdGUxDDAKBgNV 6 | BAoTA1VtVTEMMAoGA1UECxMDSVRTMREwDwYDVQQDEwhyb2hlMDAwMjCBnzANBgkq 7 | hkiG9w0BAQEFAAOBjQAwgYkCgYEA5zbNbHIYIkGGJ3RGdRKkYmF4gOorv5eDuUKT 8 | Vtuu3VvxrpOWvwnFV+NY0LgqkQSMMyVzodJE3SUuwQTUHPXXY5784vnkFqzPRx6b 9 | HgPxKz7XfwQjEBTafQTMmOeYI8wFIOIHY5i0RWR+gxDbh/D5TXuUqScOOqR47vSp 10 | IbUH+ncCAwEAATANBgkqhkiG9w0BAQUFAAOBgQDP9PE53utRqocZpFRxrhL+4vcI 11 | vlQd3XonE7vMJwdl9FUW7QRMon19dpYKQ6LTCOBA4ZCwh2z/Om2X97zogMLKFPcE 12 | LVqxzBVlrzuqAgOErEalYv9pCBzoFHGUHP6kTAUOCsoO9ZdQWgA6YkK2hYRSlLNX 13 | Z0wHIHGaHe5xnFFpyQ== 14 | -----END CERTIFICATE----- 15 | -------------------------------------------------------------------------------- /src/op/op/certs/localhost.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQDnNs1schgiQYYndEZ1EqRiYXiA6iu/l4O5QpNW267dW/Guk5a/ 3 | CcVX41jQuCqRBIwzJXOh0kTdJS7BBNQc9ddjnvzi+eQWrM9HHpseA/ErPtd/BCMQ 4 | FNp9BMyY55gjzAUg4gdjmLRFZH6DENuH8PlNe5SpJw46pHju9KkhtQf6dwIDAQAB 5 | AoGAKuSxy1KHQ6OgPaWGhKWGtXGbp17J6usy1qWRK+XpVMt/1IEw0BQB9kII8f+Y 6 | dfq//6UNBJI7kEMbn1dD+nNpF4ncO9QWHE5oqacHgaZOl6+MF3ePy8aXkADhwiel 7 | L7CtZjhwbcjGt5PI6AIcpFfmBAbu5Pf4gidr6bR+MoJGlhECQQDzfMaRqruJkqsz 8 | Z5b9boIr08orx1xPoHTmE5g0ET9+UJy/BBgx7DNv+AQhJ2UC1ZaKcgqwetOwJhQs 9 | u8Cbrct9AkEA8xiQSwqlM7ltpNl6L2VvSxzTd897it+FJElXbD6u80RvzMuo3Xw3 10 | +M+F0kDobM4vsyBuZRw418/yOpnOv8x4AwJATj5WgRDgWwEqysYLGz2bzwGsAg16 11 | eIwThKvfSTwRr0GwXSGvtLs2fFCy4wSJzTNdwPeMv9F4nS5fZVCgQGbE8QJAMZBG 12 | iyZGfH9H/Z5hrRwvTs83xmvFMpFUIgvaCTXWkb7YVJcJfO8AsngNPssBGH4Jd6ob 13 | F/5jEI1TQ+NsJerYZQJBAJdqDlnPQyqek4kdBvwh2hYo9EwOrgOchmruMOeP5lE6 14 | 2TLIyjYC3uVMPJj+ESayVcAMrgj4Enk4qh/WKVeMJ7c= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /src/op/op/config.py.example: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | keys = [ 4 | {"type": "RSA", "key": "keys/key.pem", "use": ["enc", "sig"]}, 5 | {"type": "EC", "crv": "P-256", "use": ["sig"]}, 6 | {"type": "EC", "crv": "P-256", "use": ["enc"]} 7 | ] 8 | 9 | SYM_KEY = "SoLittleTime,Got" 10 | 11 | SERVER_CERT = "certs/localhost.crt" 12 | SERVER_KEY = "certs/localhost.key" 13 | CERT_CHAIN = None 14 | 15 | STATIC_DIR = "/tmp" 16 | 17 | PASSWD = { 18 | "diana": "krall", 19 | "babs": "howes", 20 | "upper": "crust" 21 | } 22 | 23 | USERDB = { 24 | "diana": { 25 | "sub": "dikr0001", 26 | "name": "Diana Krall", 27 | "given_name": "Diana", 28 | "family_name": "Krall", 29 | "nickname": "Dina", 30 | "email": "diana@example.org", 31 | "email_verified": False, 32 | "phone_number": "+46 90 7865000", 33 | "address": { 34 | "street_address": "Umeå Universitet", 35 | "locality": "Umeå", 36 | "postal_code": "SE-90187", 37 | "country": "Sweden" 38 | }, 39 | }, 40 | "babs": { 41 | "sub": "babs0001", 42 | "name": "Barbara J Jensen", 43 | "given_name": "Barbara", 44 | "family_name": "Jensen", 45 | "nickname": "babs", 46 | "email": "babs@example.com", 47 | "email_verified": True, 48 | "address": { 49 | "street_address": "100 Universal City Plaza", 50 | "locality": "Hollywood", 51 | "region": "CA", 52 | "postal_code": "91608", 53 | "country": "USA", 54 | }, 55 | }, 56 | "upper": { 57 | "sub": "uppe0001", 58 | "name": "Upper Crust", 59 | "given_name": "Upper", 60 | "family_name": "Crust", 61 | "email": "uc@example.com", 62 | "email_verified": True, 63 | } 64 | } -------------------------------------------------------------------------------- /src/op/op/keys/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC3zCCAcegAwIBAgIBATANBgkqhkiG9w0BAQUFADA0MRgwFgYDVQQDEw9UaGUg 3 | Y29kZSB0ZXN0ZXIxGDAWBgNVBAoTD1VtZWEgVW5pdmVyc2l0eTAeFw0xMjA4MjEx 4 | MTAyMjFaFw0xMzA4MjExMTAyMjFaMDIxCzAJBgNVBAYTAlNFMSMwIQYDVQQDExpP 5 | cGVuSUQgQ29ubmVjdCBUZXN0IFNlcnZlcjCCASIwDQYJKoZIhvcNAQEBBQADggEP 6 | ADCCAQoCggEBANRAq0lVRsRw4wpvoC3RqEzwk3iIE0b4p9HBPTOrPxpA9LWt08ug 7 | wpbvBogZpfj0H0LUsmuRM7/BLlF4RYTQqUft/7zMFI2Cwcx+cmwAfzQUS/DCdTO1 8 | lM5vlkp5dYXWDKJV9cctnMtlJubGiLbmzDjYYVRvoRQLMsvYgg1/hVfGp6zj6Qgb 9 | mCtQdAWgUmnzA7rN0kfsavE+i4E3pcK97KyZukOgNgM3QbApJvlnClud+ApUWzGH 10 | zGiUJQWKZrZ0Lf5PHLVFl9ZP5CACexkeVof++eecAnFSW56HOIjYD7NSsjX5h79k 11 | YhK9pX9AZsaZM1BpzUDWjlagdfw9coLDTAMCAwEAATANBgkqhkiG9w0BAQUFAAOC 12 | AQEAIeLXKI+XukIcWHY67N9NqdWWhBi8HIygbvt6bNOlM9dFIBvc68LQIZSrJd/6 13 | AKs07N+mY9rewYFcORrgI6767Gd/vzu+HhEMpFgJnipPjBq6XuwzLSOPRHIOGMcx 14 | gLmHa/ALDhQq+ma4eghpxjYM4hY8VOXPRLzGE+WqOFc/PTUIxDRT5yk0ct+XlCdC 15 | mCRA9BnJfYG2ABsf2GtfNK2aXMIbCC/n9c/NNkffc2aXGImalj18BZzYW8TWNQt9 16 | Ypx4GMUpDZu8CLaddGHoAxZgar6+BLSwO8FWB5XiFLOH/OEai9o+LZZbSebL4fyl 17 | yST8CMkOK8hBl1tHmEWbknlH5A== 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /src/op/op/keys/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA1ECrSVVGxHDjCm+gLdGoTPCTeIgTRvin0cE9M6s/GkD0ta3T 3 | y6DClu8GiBml+PQfQtSya5Ezv8EuUXhFhNCpR+3/vMwUjYLBzH5ybAB/NBRL8MJ1 4 | M7WUzm+WSnl1hdYMolX1xy2cy2Um5saItubMONhhVG+hFAsyy9iCDX+FV8anrOPp 5 | CBuYK1B0BaBSafMDus3SR+xq8T6LgTelwr3srJm6Q6A2AzdBsCkm+WcKW534ClRb 6 | MYfMaJQlBYpmtnQt/k8ctUWX1k/kIAJ7GR5Wh/7555wCcVJbnoc4iNgPs1KyNfmH 7 | v2RiEr2lf0BmxpkzUGnNQNaOVqB1/D1ygsNMAwIDAQABAoIBAQDUMOqMX5Jl5K01 8 | y66I3+avNHtZrkAHXaL4UYVL2FE3f+SklGj+U3L1zXPsMCf7IKL3/wd3/iuL8ibK 9 | D8EALFJvtIFMT4HkjuoL9AWT71M7z2a0BNOCpG9liazoO1DAQeNTjzgsrW7o7/Da 10 | GXSn1UgpNDjpXsfb7+4SWBp8QBYgTok/dx/nZyewZsVtkCsnwrm26Y2k8nhynLm1 11 | nGkQBbRKweQJ5O3PrsrZhRO9OXJ10WK/lYHVqRnu4dam8iP8JHKz5CQN/O7x0HI5 12 | VHHAl0Az4+YZe8wrdNhgtkkOhGxwOtIK5Q3eV8st51YBpWbMiDtQ53ghisWtyNzg 13 | EyphOksxAoGBAO1bTWHsuo6srY4cEJJlJ/C7W237iUM2p9MkeOwcewSXtgY19jAb 14 | IalYzn+yIHLqMyJ1H4cA1sBXBooNvkjVd+7niBbE8du2LyHf4avhNHN2tgKIOjpj 15 | GCsitLEqhyQmUUlYC3z8tQbh99b7fqn2kBDnhdSWPojEx84eXQ71IhMPAoGBAOTs 16 | lQ2iT3cqyore02YOieDwSg7n4flAPPG6cTGGQeOyHpNllTy9U/wyEBrHGGSCkjHI 17 | uDICHtYxyBmL6b0H5IyT4vn1Wxm19ecy8zbuK7Lmwd/iWRfKlGr+YQuez/mHVcCG 18 | YMoQvS6j8WOrQxt2wdOMNatgJeJSz66TaRy2QWfNAoGBAOmTWNJN8MSon15itdgq 19 | 3aQj6/SOfOR866h3ktvfpxu85C62eZ+bg4OwVf4J367WVB3LnovvQmYi/ddrcN8h 20 | 2xVqGV020D+DyFwQgnbvdvtNTg2t24dLryP70k8qZ7UmVAXWM+/6i3bLdmbENUCy 21 | 19Ea1XN/quhSpcFr1e37Q133AoGAD5GLXX8BWoBdf+5BgDpS5CpTTwo0EwhsXKAq 22 | XIzd5EdTzwBkktnpYUhiUf/iR8udd6dH55a/VB/UlPAv+DwWLf1MvWUTSf9W9t8/ 23 | LSgrbqJE4x34oyaSy2f7X5fwWu76RPqekH9s7kQWAYo/KRn9eo6Zg8spKGgrWZsK 24 | 1foLHq0CgYBJRjKEY79aNuKCJZw60QPpXodJ65RJufXPz9MgDdoxUOtno8eYPfep 25 | KWWyhJsQXhMJNUMZGvQXRXaaZ3ZZp1e1q18CLh1TqbInC1ODW3L/ZAWCpT9ihcdA 26 | Owj1RL042er59qut/nivipmB5fn1hTbRDLq9rng0fsNU9XlrETbUfg== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /src/op/op/op.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from functools import partial 3 | import importlib 4 | import json 5 | import os 6 | import shelve 7 | import urlparse 8 | import sys 9 | 10 | from cherrypy import wsgiserver 11 | import cherrypy 12 | from cherrypy.wsgiserver import ssl_pyopenssl 13 | from mako.lookup import TemplateLookup 14 | from oic.oic.non_web_provider import NonWebProvider, MakoRenderer 15 | from oic.oic.provider import AuthorizationEndpoint, TokenEndpoint, \ 16 | UserinfoEndpoint, RegistrationEndpoint, EndSessionEndpoint 17 | from oic.utils.authn.authn_context import AuthnBroker 18 | from oic.utils.authn.client import verify_client 19 | from oic.utils.authn.user import UsernamePasswordMako 20 | from oic.utils.authz import AuthzHandling 21 | from oic.utils.http_util import wsgi_wrapper, ServiceError, BadRequest, \ 22 | Response, \ 23 | get_or_post 24 | from oic.utils.keyio import keyjar_init 25 | from oic.utils.sdb import SessionDB 26 | from oic.utils.userinfo import UserInfo 27 | from oic.utils.webfinger import OIC_ISSUER, WebFinger 28 | 29 | 30 | class OICProviderMiddleware(object): 31 | def __init__(self, provider, app): 32 | self.provider = provider 33 | self.app = app 34 | 35 | def __call__(self, environ, start_response): 36 | environ["oic.oas"] = self.provider 37 | return self.app(environ, start_response) 38 | 39 | 40 | def token(environ, start_response): 41 | _oas = environ["oic.oas"] 42 | return wsgi_wrapper(environ, start_response, _oas.token_endpoint) 43 | 44 | 45 | def authorization(environ, start_response): 46 | _oas = environ["oic.oas"] 47 | return wsgi_wrapper(environ, start_response, _oas.authorization_endpoint) 48 | 49 | 50 | def userinfo(environ, start_response): 51 | _oas = environ["oic.oas"] 52 | return wsgi_wrapper(environ, start_response, _oas.userinfo_endpoint) 53 | 54 | 55 | def op_info(environ, start_response): 56 | _oas = environ["oic.oas"] 57 | return wsgi_wrapper(environ, start_response, _oas.providerinfo_endpoint) 58 | 59 | 60 | def registration(environ, start_response): 61 | _oas = environ["oic.oas"] 62 | 63 | if environ["REQUEST_METHOD"] == "POST": 64 | return wsgi_wrapper(environ, start_response, _oas.registration_endpoint) 65 | elif environ["REQUEST_METHOD"] == "GET": 66 | return wsgi_wrapper(environ, start_response, _oas.read_registration) 67 | else: 68 | resp = ServiceError("Method not supported") 69 | return resp(environ, start_response) 70 | 71 | 72 | def endsession(environ, start_response): 73 | _oas = environ["oic.oas"] 74 | 75 | return wsgi_wrapper(environ, start_response, _oas.endsession_endpoint) 76 | 77 | 78 | def webfinger(environ, start_response): 79 | query = urlparse.parse_qs(environ["QUERY_STRING"]) 80 | try: 81 | assert query["rel"] == [OIC_ISSUER] 82 | resource = query["resource"][0] 83 | except KeyError: 84 | resp = BadRequest("Missing parameter in request") 85 | else: 86 | wf = WebFinger() 87 | resp = Response(wf.response(subject=resource, base=OAS.baseurl)) 88 | return resp(environ, start_response) 89 | 90 | 91 | def consent(environ, start_response): 92 | _oas = environ["oic.oas"] 93 | 94 | params_str = get_or_post(environ) 95 | params = dict(urlparse.parse_qsl(params_str)) 96 | 97 | return _oas.consent_endpoint(**params)(environ, start_response) 98 | 99 | 100 | def list_access_tokens(environ, start_response): 101 | _oas = environ["oic.oas"] 102 | return wsgi_wrapper(environ, start_response, 103 | _oas.list_access_tokens_endpoint) 104 | 105 | 106 | def revoke_access_token(environ, start_response): 107 | _oas = environ["oic.oas"] 108 | 109 | params_str = get_or_post(environ) 110 | params = dict(urlparse.parse_qsl(params_str)) 111 | 112 | return _oas.revoke_access_token_endpoint(**params)(environ, start_response) 113 | 114 | 115 | ENDPOINTS = [ 116 | AuthorizationEndpoint(authorization), 117 | TokenEndpoint(token), 118 | UserinfoEndpoint(userinfo), 119 | RegistrationEndpoint(registration), 120 | EndSessionEndpoint(endsession) 121 | ] 122 | 123 | LOOKUP = TemplateLookup(directories=["../templates"], input_encoding='utf-8', 124 | output_encoding='utf-8') 125 | 126 | if __name__ == '__main__': 127 | parser = argparse.ArgumentParser() 128 | parser.add_argument('-p', dest='port', default=80, type=int) 129 | parser.add_argument("-b", required=True, dest="base", 130 | help="base url of the provider") 131 | parser.add_argument(dest="config") 132 | args = parser.parse_args() 133 | 134 | # Client data base 135 | cdb = shelve.Shelf({}) 136 | 137 | sys.path.insert(0, ".") 138 | config = importlib.import_module(args.config) 139 | baseurl = "{base}:{port}".format(base=args.base.rstrip("/"), port=args.port) 140 | 141 | ac = AuthnBroker() 142 | authn = UsernamePasswordMako(None, "login.mako", LOOKUP, config.PASSWD, 143 | "%s/authorization" % baseurl) 144 | ac.add("UsernamePassword", authn) 145 | 146 | # dealing with authorization 147 | authz = AuthzHandling() 148 | 149 | # Consent and list tokens page 150 | renderer = MakoRenderer(LOOKUP) 151 | consent_page_handler = partial(renderer, "consent.mako", 152 | form_action="/consent_ok") 153 | list_tokens_page_handler = partial(renderer, "list_access_tokens.mako") 154 | 155 | OAS = NonWebProvider(baseurl, SessionDB(baseurl), cdb, ac, 156 | None, authz, verify_client, config.SYM_KEY, 157 | consent_page_handler, list_tokens_page_handler) 158 | 159 | for authn in ac: 160 | authn.srv = OAS 161 | 162 | # User info is a simple dictionary in this case statically defined in 163 | # the configuration file 164 | OAS.userinfo = UserInfo(config.USERDB) 165 | OAS.endpoints = ENDPOINTS 166 | OAS.baseurl = baseurl 167 | 168 | try: 169 | jwks = keyjar_init(OAS, config.keys, kid_template="op%d") 170 | except Exception, err: 171 | OAS.key_setup("static", sig={"format": "jwk", "alg": "rsa"}) 172 | else: 173 | new_name = "jwks.json" 174 | with open(os.path.join(config.STATIC_DIR, new_name), "w") as f: 175 | json.dump(jwks, f) 176 | OAS.jwks_uri.append("%s/static/%s" % (OAS.baseurl, new_name)) 177 | 178 | # Static file handling 179 | static_config = { 180 | "/static": { 181 | "tools.staticdir.on": True, 182 | "tools.staticdir.dir": config.STATIC_DIR 183 | } 184 | } 185 | static_handler = cherrypy.tree.mount(None, "/", config=static_config) 186 | 187 | # Setup endpoints 188 | all_endpoints = [ 189 | ("/.well-known/openid-configuration", 190 | OICProviderMiddleware(OAS, op_info)), 191 | ("/.well-known/webfinger", OICProviderMiddleware(OAS, webfinger)), 192 | ("/verify", 193 | lambda environ, start_response: wsgi_wrapper(environ, start_response, 194 | authn.verify)), 195 | ("/my_tokens", 196 | OICProviderMiddleware(OAS, list_access_tokens)), 197 | ("/consent_ok", OICProviderMiddleware(OAS, consent)), 198 | ("/revoke_token", OICProviderMiddleware(OAS, revoke_access_token)), 199 | ("/static", static_handler) 200 | ] 201 | for ep in ENDPOINTS: 202 | all_endpoints.append(("/%s" % ep.etype, OICProviderMiddleware(OAS, ep))) 203 | 204 | d = wsgiserver.WSGIPathInfoDispatcher(all_endpoints) 205 | SRV = wsgiserver.CherryPyWSGIServer(('0.0.0.0', args.port), d) 206 | 207 | https = "" 208 | if baseurl.startswith("https"): 209 | https = "using HTTPS" 210 | SRV.ssl_adapter = ssl_pyopenssl.pyOpenSSLAdapter( 211 | config.SERVER_CERT, config.SERVER_KEY, config.CERT_CHAIN) 212 | 213 | print "OC server starting listening on port:%s %s" % (args.port, https) 214 | try: 215 | SRV.start() 216 | except KeyboardInterrupt: 217 | SRV.stop() -------------------------------------------------------------------------------- /src/op/requirements.txt: -------------------------------------------------------------------------------- 1 | git+https://github.com/its-dirg/pyoidc@non-web 2 | cherrypy==3.2.4 -------------------------------------------------------------------------------- /src/op/templates/consent.mako: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Name your access token 7 | 8 | 9 |
10 |

Name your access token

11 | By naming your access token you authorize ${client} to use it. 12 | Make sure the nickname for your access token is unique! 13 |
14 | 15 |
16 | 17 | 18 | 19 | 21 | 22 | 23 |
-------------------------------------------------------------------------------- /src/op/templates/list_access_tokens.mako: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
    9 | % for nick in access_tokens: 10 |
  • ${nick}: 11 | Revoke 12 |
    ${access_tokens[nick]}
    13 |
  • 14 | % endfor 15 | 16 |
17 | 18 | -------------------------------------------------------------------------------- /src/op/templates/login.mako: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Please login 7 | 8 | 20 | 21 | 22 | 23 |
24 |

${title}

25 |
26 | 27 |
28 | 46 |
47 | 48 | -------------------------------------------------------------------------------- /src/pam_module/pam_oidc_authz.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Requires 'libpam0g-dev' and 'libcurl4-openssl-dev' on Ubuntu. 3 | * 4 | * Setup: 5 | * mkdir /lib/security 6 | * gcc -fPIC -fno-stack-protector -c pam_oidc_authz.c 7 | * ld -x --shared -o /lib/security/pam_oidc_authz.so pam_oidc_authz.o `curl-config --libs` 8 | */ 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | 19 | /* 20 | * Callback for libcurl to handle data. 21 | * Does nothing, instead of default behaviour to print to stdout. 22 | */ 23 | size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) { 24 | return size * nmemb; 25 | } 26 | 27 | /* 28 | * Verify the access token. 29 | */ 30 | int verify_access_token(const char* username, const char* access_token, 31 | const char* url, int verify_ssl) { 32 | CURL *curl = curl_easy_init(); 33 | if (!curl) { 34 | return PAM_SYSTEM_ERR; 35 | } 36 | 37 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); 38 | /* url may be redirected, so we tell libcurl to follow redirection */ 39 | curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); 40 | if (!verify_ssl) { 41 | curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); 42 | curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); 43 | } 44 | 45 | /* Encode parameters */ 46 | char* encoded_username = curl_easy_escape(curl, username, 0); 47 | char* encoded_access_token = curl_easy_escape(curl, access_token, 0); 48 | if (!encoded_username || ! encoded_access_token) { 49 | return PAM_SYSTEM_ERR; 50 | } 51 | 52 | /* Build url */ 53 | char full_url[1024]; 54 | snprintf(full_url, sizeof(full_url), "%s?user=%s&access_token=%s", url, 55 | encoded_username, encoded_access_token); 56 | curl_free(encoded_username); 57 | curl_free(encoded_access_token); 58 | 59 | printf("Full url: %s\n", full_url); 60 | curl_easy_setopt(curl, CURLOPT_URL, full_url); 61 | 62 | CURLcode res = curl_easy_perform(curl); 63 | /* Check for errors */ 64 | if(res != CURLE_OK) 65 | fprintf(stderr, "curl_easy_perform() failed: %s\n", 66 | curl_easy_strerror(res)); 67 | 68 | long http_code = 0; 69 | curl_easy_getinfo (curl, CURLINFO_RESPONSE_CODE, &http_code); 70 | curl_easy_cleanup(curl); 71 | 72 | return http_code == 200; 73 | } 74 | 75 | /* 76 | * PAM module callback for 'auth' 77 | */ 78 | PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, 79 | const char **argv ) { 80 | int retval; 81 | 82 | if (argc < 1) { 83 | return PAM_SYSTEM_ERR; // fail instantly if not configured with url 84 | } 85 | 86 | const char* p_user; 87 | retval = pam_get_user(pamh, &p_user, "Username: "); 88 | if (retval != PAM_SUCCESS) { 89 | return retval; 90 | } 91 | 92 | const char* p_access_token; 93 | retval = pam_get_authtok(pamh, PAM_AUTHTOK, &p_access_token, "Access token: "); 94 | if (retval != PAM_SUCCESS) { 95 | return retval; 96 | } 97 | 98 | int verify_ssl = 1; 99 | if (argc == 2) { 100 | verify_ssl = atoi(argv[1]); 101 | } 102 | 103 | return verify_access_token(p_user, p_access_token, argv[0], verify_ssl) ? 104 | PAM_SUCCESS : PAM_AUTH_ERR; 105 | } 106 | 107 | /* 108 | * PAM module callback for 'auth' 109 | */ 110 | PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, 111 | const char **argv ) { 112 | return PAM_SUCCESS; 113 | } -------------------------------------------------------------------------------- /src/service_provider/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'danielevertsson' 2 | -------------------------------------------------------------------------------- /src/service_provider/certs/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICGTCCAYICCQCIs8fF+AnxUzANBgkqhkiG9w0BAQUFADBRMQswCQYDVQQGEwJT 3 | RTETMBEGA1UECBMKU29tZS1TdGF0ZTEMMAoGA1UEChMDVW1VMQwwCgYDVQQLEwNJ 4 | VFMxETAPBgNVBAMTCHJvaGUwMDAyMB4XDTExMDkwNzA4MjMwOFoXDTEyMDkwNjA4 5 | MjMwOFowUTELMAkGA1UEBhMCU0UxEzARBgNVBAgTClNvbWUtU3RhdGUxDDAKBgNV 6 | BAoTA1VtVTEMMAoGA1UECxMDSVRTMREwDwYDVQQDEwhyb2hlMDAwMjCBnzANBgkq 7 | hkiG9w0BAQEFAAOBjQAwgYkCgYEA5zbNbHIYIkGGJ3RGdRKkYmF4gOorv5eDuUKT 8 | Vtuu3VvxrpOWvwnFV+NY0LgqkQSMMyVzodJE3SUuwQTUHPXXY5784vnkFqzPRx6b 9 | HgPxKz7XfwQjEBTafQTMmOeYI8wFIOIHY5i0RWR+gxDbh/D5TXuUqScOOqR47vSp 10 | IbUH+ncCAwEAATANBgkqhkiG9w0BAQUFAAOBgQDP9PE53utRqocZpFRxrhL+4vcI 11 | vlQd3XonE7vMJwdl9FUW7QRMon19dpYKQ6LTCOBA4ZCwh2z/Om2X97zogMLKFPcE 12 | LVqxzBVlrzuqAgOErEalYv9pCBzoFHGUHP6kTAUOCsoO9ZdQWgA6YkK2hYRSlLNX 13 | Z0wHIHGaHe5xnFFpyQ== 14 | -----END CERTIFICATE----- 15 | -------------------------------------------------------------------------------- /src/service_provider/certs/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQDnNs1schgiQYYndEZ1EqRiYXiA6iu/l4O5QpNW267dW/Guk5a/ 3 | CcVX41jQuCqRBIwzJXOh0kTdJS7BBNQc9ddjnvzi+eQWrM9HHpseA/ErPtd/BCMQ 4 | FNp9BMyY55gjzAUg4gdjmLRFZH6DENuH8PlNe5SpJw46pHju9KkhtQf6dwIDAQAB 5 | AoGAKuSxy1KHQ6OgPaWGhKWGtXGbp17J6usy1qWRK+XpVMt/1IEw0BQB9kII8f+Y 6 | dfq//6UNBJI7kEMbn1dD+nNpF4ncO9QWHE5oqacHgaZOl6+MF3ePy8aXkADhwiel 7 | L7CtZjhwbcjGt5PI6AIcpFfmBAbu5Pf4gidr6bR+MoJGlhECQQDzfMaRqruJkqsz 8 | Z5b9boIr08orx1xPoHTmE5g0ET9+UJy/BBgx7DNv+AQhJ2UC1ZaKcgqwetOwJhQs 9 | u8Cbrct9AkEA8xiQSwqlM7ltpNl6L2VvSxzTd897it+FJElXbD6u80RvzMuo3Xw3 10 | +M+F0kDobM4vsyBuZRw418/yOpnOv8x4AwJATj5WgRDgWwEqysYLGz2bzwGsAg16 11 | eIwThKvfSTwRr0GwXSGvtLs2fFCy4wSJzTNdwPeMv9F4nS5fZVCgQGbE8QJAMZBG 12 | iyZGfH9H/Z5hrRwvTs83xmvFMpFUIgvaCTXWkb7YVJcJfO8AsngNPssBGH4Jd6ob 13 | F/5jEI1TQ+NsJerYZQJBAJdqDlnPQyqek4kdBvwh2hYo9EwOrgOchmruMOeP5lE6 14 | 2TLIyjYC3uVMPJj+ESayVcAMrgj4Enk4qh/WKVeMJ7c= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /src/service_provider/conf.py.example: -------------------------------------------------------------------------------- 1 | # If https is to be used these has to be specified 2 | SERVER_CERT = "certs/server.crt" 3 | SERVER_KEY = "certs/server.key" 4 | CA_BUNDLE = None 5 | 6 | VERIFY_SSL = False 7 | 8 | # information used when registering the client 9 | 10 | ME = { 11 | "application_type": "web", 12 | "redirect_uris": ["%sauthz_cb" % BASE], 13 | "response_types": ["code"] 14 | } 15 | 16 | PAM_DATABASE = "pam.db" 17 | 18 | BEHAVIOUR = { 19 | "response_type": "code", 20 | "scope": ["openid", "auth_test"], 21 | } 22 | 23 | CLIENTS = { 24 | "": { 25 | "client_info": ME, 26 | "behaviour": BEHAVIOUR 27 | }, 28 | } -------------------------------------------------------------------------------- /src/service_provider/database.py: -------------------------------------------------------------------------------- 1 | import dataset 2 | from prettytable import PrettyTable 3 | 4 | __author__ = 'danielevertsson' 5 | 6 | ISSUER_COLUMN = "issuer" 7 | LOCAL_USER_COLUMN = "local_user" 8 | SUBECT_ID_COLUMN = "subject_id" 9 | 10 | class PamDatabase(): 11 | TABLE_NAME = 'pam' 12 | 13 | def __init__(self, dict_path): 14 | self.database = dataset.connect('sqlite:///' + dict_path) 15 | self.table = self.database[self.TABLE_NAME] 16 | 17 | def clear(self): 18 | self.table.drop() 19 | self.table = self.database[self.TABLE_NAME] 20 | 21 | def upsert(self, issuer, local_user, subject_id): 22 | row = {ISSUER_COLUMN: issuer, 23 | LOCAL_USER_COLUMN: local_user, 24 | SUBECT_ID_COLUMN: subject_id} 25 | self.table.upsert(row, [LOCAL_USER_COLUMN]) 26 | 27 | def get_row(self, local_user): 28 | return self.table.find_one(local_user=local_user) 29 | 30 | def get_table_as_list(self): 31 | list = [] 32 | rows = self.table.find(order_by=[SUBECT_ID_COLUMN]) 33 | for row in rows: 34 | list.append([row[SUBECT_ID_COLUMN], row[LOCAL_USER_COLUMN], row[ISSUER_COLUMN]]) 35 | return list 36 | 37 | def print_table(self): 38 | list =self.get_table_as_list() 39 | table = PrettyTable([SUBECT_ID_COLUMN, LOCAL_USER_COLUMN, ISSUER_COLUMN]) 40 | table.padding_width = 1 41 | 42 | for row in list: 43 | list = [] 44 | for element in row: 45 | if isinstance(element, int): 46 | list.append(element) 47 | else: 48 | list.append(element.encode('utf8')) 49 | table.add_row(list) 50 | print table -------------------------------------------------------------------------------- /src/service_provider/htdocs/access_token.mako: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | pyoidc RP 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 38 | 39 |
40 | 41 |
42 |

Access token

43 | Access token received from the server: 44 | 47 |
48 | 49 |
50 | 51 | 52 | 53 | % if check_session_iframe_url is not UNDEFINED: 54 | 55 | 56 | % endif 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/service_provider/htdocs/opchoice.mako: -------------------------------------------------------------------------------- 1 | <%! 2 | def op_choice(op_list): 3 | #Creates a dropdown list of OpenID Connect providers 4 | element = "" 8 | return element 9 | %> 10 | 11 | 12 | 13 | 14 | 15 | pyoidc RP 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 35 | 36 |
37 | 38 |
39 | 50 |
51 | 52 |
53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/service_provider/htdocs/operror.mako: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | pyoidc RP 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 38 | 39 |
40 | 41 |
42 |

OP result

43 | 44 |

You have failed to connect to the designated OP with the message:

45 | 46 |

${error}

47 |
48 | 49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/service_provider/htdocs/rp_session_iframe.mako: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/service_provider/oidc.py: -------------------------------------------------------------------------------- 1 | from oic.utils.http_util import Redirect 2 | from oic.exception import MissingAttribute 3 | from oic import oic 4 | from oic.oauth2 import rndstr, ErrorResponse 5 | from oic.oic import ProviderConfigurationResponse, AuthorizationResponse 6 | from oic.oic import RegistrationResponse 7 | from oic.oic import AuthorizationRequest 8 | from oic.utils.authn.client import CLIENT_AUTHN_METHOD 9 | 10 | __author__ = 'roland' 11 | 12 | import logging 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class OIDCError(Exception): 18 | pass 19 | 20 | 21 | class Client(oic.Client): 22 | def __init__(self, client_id=None, ca_certs=None, 23 | client_prefs=None, client_authn_method=None, keyjar=None, 24 | verify_ssl=True, behaviour=None): 25 | oic.Client.__init__(self, client_id, ca_certs, client_prefs, 26 | client_authn_method, keyjar, verify_ssl) 27 | if behaviour: 28 | self.behaviour = behaviour 29 | 30 | def create_authn_request(self, session, acr_value=None, **kwargs): 31 | session["state"] = rndstr() 32 | session["nonce"] = rndstr() 33 | request_args = { 34 | "response_type": self.behaviour["response_type"], 35 | "scope": self.behaviour["scope"], 36 | "state": session["state"], 37 | "nonce": session["nonce"], 38 | "redirect_uri": self.registration_response["redirect_uris"][0] 39 | } 40 | 41 | if acr_value is not None: 42 | request_args["acr_values"] = acr_value 43 | 44 | request_args.update(kwargs) 45 | cis = self.construct_AuthorizationRequest(request_args=request_args) 46 | logger.debug("request: %s" % cis) 47 | 48 | url, body, ht_args, cis = self.uri_and_body(AuthorizationRequest, cis, 49 | method="GET", 50 | request_args=request_args) 51 | 52 | logger.debug("body: %s" % body) 53 | logger.info("URL: %s" % url) 54 | logger.debug("ht_args: %s" % ht_args) 55 | 56 | resp = Redirect(str(url)) 57 | if ht_args: 58 | resp.headers.extend([(a, b) for a, b in ht_args.items()]) 59 | logger.debug("resp_headers: %s" % resp.headers) 60 | return resp 61 | 62 | def callback(self, response, session): 63 | """ 64 | This is the method that should be called when an AuthN response has been 65 | received from the OP. 66 | 67 | :param response: The URL returned by the OP 68 | :return: 69 | """ 70 | authresp = self.parse_response(AuthorizationResponse, response, 71 | sformat="dict", keyjar=self.keyjar) 72 | 73 | if isinstance(authresp, ErrorResponse): 74 | if authresp["error"] == "login_required": 75 | return self.create_authn_request(session) 76 | else: 77 | return OIDCError("Access denied") 78 | 79 | if session["state"] != authresp["state"]: 80 | return OIDCError("Received state not the same as expected.") 81 | 82 | try: 83 | if authresp["id_token"] != session["nonce"]: 84 | return OIDCError("Received nonce not the same as expected.") 85 | self.id_token[authresp["state"]] = authresp["id_token"] 86 | except KeyError: 87 | pass 88 | 89 | if self.behaviour["response_type"] == "code": 90 | # get the access token 91 | try: 92 | args = { 93 | "code": authresp["code"], 94 | "redirect_uri": self.registration_response[ 95 | "redirect_uris"][0], 96 | "client_id": self.client_id, 97 | "client_secret": self.client_secret 98 | } 99 | 100 | access_token_response = self.do_access_token_request( 101 | scope="openid", state=authresp["state"], request_args=args, 102 | authn_method=self.registration_response["token_endpoint_auth_method"]) 103 | except Exception as err: 104 | logger.error("%s" % err) 105 | raise 106 | 107 | if isinstance(access_token_response, ErrorResponse): 108 | raise OIDCError("Invalid response %s." % access_token_response["error"]) 109 | 110 | return access_token_response 111 | 112 | def request_user_info(self, access_token): 113 | inforesp = self.do_user_info_request(**{"access_token": access_token}) 114 | 115 | if isinstance(inforesp, ErrorResponse): 116 | raise OIDCError("Invalid response %s." % inforesp["error"]) 117 | 118 | userinfo = inforesp.to_dict() 119 | 120 | logger.debug("UserInfo: %s" % inforesp) 121 | 122 | return userinfo 123 | 124 | 125 | 126 | class OIDCClients(object): 127 | def __init__(self, config): 128 | """ 129 | 130 | :param config: Imported configuration module 131 | :return: 132 | """ 133 | self.client = {} 134 | self.client_cls = Client 135 | self.config = config 136 | 137 | for key, val in config.CLIENTS.items(): 138 | if key == "": 139 | continue 140 | else: 141 | self.client[key] = self.create_client(**val) 142 | 143 | def create_client(self, userid="", **kwargs): 144 | """ 145 | Do an instantiation of a client instance 146 | 147 | :param userid: An identifier of the user 148 | :param: Keyword arguments 149 | Keys are ["srv_discovery_url", "client_info", "client_registration", 150 | "provider_info"] 151 | :return: client instance 152 | """ 153 | 154 | _key_set = set(kwargs.keys()) 155 | args = {} 156 | for param in ["verify_ssl"]: 157 | try: 158 | args[param] = kwargs[param] 159 | except KeyError: 160 | pass 161 | else: 162 | _key_set.discard(param) 163 | 164 | client = self.client_cls(client_authn_method=CLIENT_AUTHN_METHOD, 165 | behaviour=kwargs["behaviour"], verify_ssl=self.config.VERIFY_SSL, **args) 166 | 167 | # The behaviour parameter is not significant for the election process 168 | _key_set.discard("behaviour") 169 | for param in ["allow"]: 170 | try: 171 | setattr(client, param, kwargs[param]) 172 | except KeyError: 173 | pass 174 | else: 175 | _key_set.discard(param) 176 | 177 | if _key_set == set(["client_info"]): # Everything dynamic 178 | # There has to be a userid 179 | if not userid: 180 | raise MissingAttribute("Missing userid specification") 181 | 182 | # Find the service that provides information about the OP 183 | issuer = client.wf.discovery_query(userid) 184 | # Gather OP information 185 | _ = client.provider_config(issuer) 186 | # register the client 187 | _ = client.register(client.provider_info["registration_endpoint"], 188 | **kwargs["client_info"]) 189 | elif _key_set == set(["client_info", "srv_discovery_url"]): 190 | # Ship the webfinger part 191 | # Gather OP information 192 | _ = client.provider_config(kwargs["srv_discovery_url"]) 193 | # register the client 194 | _ = client.register(client.provider_info["registration_endpoint"], 195 | **kwargs["client_info"]) 196 | elif _key_set == set(["provider_info", "client_info"]): 197 | client.handle_provider_config( 198 | ProviderConfigurationResponse(**kwargs["provider_info"]), 199 | kwargs["provider_info"]["issuer"]) 200 | _ = client.register(client.provider_info["registration_endpoint"], 201 | **kwargs["client_info"]) 202 | elif _key_set == set(["provider_info", "client_registration"]): 203 | client.handle_provider_config( 204 | ProviderConfigurationResponse(**kwargs["provider_info"]), 205 | kwargs["provider_info"]["issuer"]) 206 | client.store_registration_info(RegistrationResponse( 207 | **kwargs["client_registration"])) 208 | elif _key_set == set(["srv_discovery_url", "client_registration"]): 209 | _ = client.provider_config(kwargs["srv_discovery_url"]) 210 | client.store_registration_info(RegistrationResponse( 211 | **kwargs["client_registration"])) 212 | else: 213 | raise Exception("Configuration error ?") 214 | 215 | return client 216 | 217 | def dynamic_client(self, userid): 218 | client = self.client_cls(client_authn_method=CLIENT_AUTHN_METHOD, 219 | verify_ssl=self.config.VERIFY_SSL) 220 | 221 | issuer = client.wf.discovery_query(userid) 222 | if issuer in self.client: 223 | return self.client[issuer] 224 | else: 225 | # Gather OP information 226 | _pcr = client.provider_config(issuer) 227 | # register the client 228 | _ = client.register(_pcr["registration_endpoint"], 229 | **self.config.CLIENTS[""]["client_info"]) 230 | try: 231 | client.behaviour.update(**self.config.CLIENTS[""]["behaviour"]) 232 | except KeyError: 233 | pass 234 | 235 | self.client[issuer] = client 236 | return client 237 | 238 | def __getitem__(self, item): 239 | """ 240 | Given a service or user identifier return a suitable client 241 | :param item: 242 | :return: 243 | """ 244 | try: 245 | return self.client[item] 246 | except KeyError: 247 | return self.dynamic_client(item) 248 | 249 | def keys(self): 250 | return self.client.keys() 251 | -------------------------------------------------------------------------------- /src/service_provider/requirements.txt: -------------------------------------------------------------------------------- 1 | dataset 2 | prettytable 3 | -------------------------------------------------------------------------------- /src/service_provider/service_provider.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import importlib 3 | import urlparse 4 | from urlparse import parse_qs 5 | import logging 6 | import argparse 7 | from mako.lookup import TemplateLookup 8 | from oic.utils.http_util import NotFound, Unauthorized, ServiceError 9 | from oic.utils.http_util import Response 10 | from oic.utils.http_util import Redirect 11 | from database import PamDatabase 12 | from oidc import OIDCClients 13 | from oidc import OIDCError 14 | from beaker.middleware import SessionMiddleware 15 | from cherrypy import wsgiserver 16 | 17 | 18 | def setup_logger(): 19 | global LOGGER, LOGFILE_NAME, hdlr, base_formatter 20 | LOGGER = logging.getLogger("") 21 | LOGFILE_NAME = 'service_provider.log' 22 | hdlr = logging.FileHandler(LOGFILE_NAME) 23 | base_formatter = logging.Formatter( 24 | "%(asctime)s %(name)s:%(levelname)s %(message)s") 25 | hdlr.setFormatter(base_formatter) 26 | LOGGER.addHandler(hdlr) 27 | LOGGER.setLevel(logging.DEBUG) 28 | 29 | 30 | setup_logger() 31 | 32 | LOOKUP = TemplateLookup(directories=['templates', 'htdocs'], 33 | module_directory='modules', 34 | input_encoding='utf-8', 35 | output_encoding='utf-8') 36 | 37 | # noinspection PyUnresolvedReferences 38 | def static(environ, start_response, logger, path): 39 | logger.info("[static]sending: %s" % (path,)) 40 | 41 | try: 42 | text = open(path).read() 43 | if path.endswith(".ico"): 44 | start_response('200 OK', [('Content-Type', "image/x-icon")]) 45 | elif path.endswith(".html"): 46 | start_response('200 OK', [('Content-Type', 'text/html')]) 47 | elif path.endswith(".json"): 48 | start_response('200 OK', [('Content-Type', 'application/json')]) 49 | elif path.endswith(".txt"): 50 | start_response('200 OK', [('Content-Type', 'text/plain')]) 51 | elif path.endswith(".css"): 52 | start_response('200 OK', [('Content-Type', 'text/css')]) 53 | else: 54 | start_response('200 OK', [('Content-Type', "text/xml")]) 55 | return [text] 56 | except IOError: 57 | resp = NotFound() 58 | return resp(environ, start_response) 59 | 60 | 61 | def opchoice(environ, start_response, clients): 62 | resp = Response(mako_template="opchoice.mako", 63 | template_lookup=LOOKUP, 64 | headers=[]) 65 | argv = { 66 | "op_list": clients.keys() 67 | } 68 | return resp(environ, start_response, **argv) 69 | 70 | 71 | def access_token_page(environ, start_response, access_token): 72 | resp = Response(mako_template="access_token.mako", 73 | template_lookup=LOOKUP, 74 | headers=[]) 75 | argv = { 76 | "access_token": access_token, 77 | } 78 | 79 | return resp(environ, start_response, **argv) 80 | 81 | 82 | def operror(environ, start_response, error=None): 83 | resp = Response(mako_template="operror.mako", 84 | template_lookup=LOOKUP, 85 | headers=[]) 86 | argv = { 87 | "error": error 88 | } 89 | return resp(environ, start_response, **argv) 90 | 91 | 92 | def verify_userinfo(row, user_info): 93 | if user_info["sub"] == row["subject_id"]: 94 | for scope in conf.BEHAVIOUR["scope"]: 95 | if scope in user_info: 96 | return Response() 97 | 98 | error_message = "No auth claim in user info response (%s)" % user_info.keys() 99 | LOGGER.error(error_message) 100 | return ServiceError(error_message) 101 | 102 | error_message = "Logged in user (%s) does not match the one stored in the database (%s)" % ( 103 | user_info["sub"], row["subject_id"]) 104 | LOGGER.debug(error_message) 105 | return Unauthorized(error_message) 106 | 107 | 108 | def verify_access_token(query): 109 | local_user = query['user'][0] 110 | row = DATABASE.get_row(local_user) 111 | 112 | if not row: 113 | error_message = "No local user (%s) found in the database." % local_user 114 | LOGGER.error(error_message) 115 | return ServiceError(error_message) 116 | 117 | access_token = query['access_token'][0] 118 | client = CLIENTS[row["issuer"]] 119 | user_info = client.request_user_info(access_token) 120 | 121 | return verify_userinfo(row, user_info) 122 | 123 | 124 | def application(environ, start_response): 125 | session = environ['beaker.session'] 126 | path = environ.get('PATH_INFO', '').lstrip('/') 127 | query = parse_qs(environ["QUERY_STRING"]) 128 | 129 | if path == "robots.txt": 130 | return static(environ, start_response, LOGGER, "static/robots.txt") 131 | 132 | if path.startswith("static/"): 133 | return static(environ, start_response, LOGGER, path) 134 | 135 | if path == "rp": # After having chosen which OP to authenticate at 136 | if "uid" in query: 137 | client = CLIENTS.dynamic_client(query["uid"][0]) 138 | session["op"] = client.provider_info["issuer"] 139 | else: 140 | client = CLIENTS[query["op"][0]] 141 | session["op"] = query["op"][0] 142 | 143 | try: 144 | resp = client.create_authn_request(session) 145 | except Exception: 146 | raise 147 | else: 148 | return resp(environ, start_response) 149 | elif path == "authz_cb": # After having authenticated at the OP 150 | client = CLIENTS[session["op"]] 151 | try: 152 | result = client.callback(query, session) 153 | if isinstance(result, Redirect): 154 | return result(environ, start_response) 155 | except OIDCError as err: 156 | return operror(environ, start_response, "%s" % err) 157 | except Exception as ex: 158 | raise 159 | else: 160 | DATABASE.upsert(issuer=session["op"], 161 | local_user=session['local_username'], 162 | subject_id=result['id_token']['sub']) 163 | return access_token_page(environ, start_response, 164 | result['access_token']) 165 | elif path == "verify_access_token": 166 | response = verify_access_token(query) 167 | return response(environ, start_response) 168 | 169 | if "username" in query: 170 | session['local_username'] = query['username'][0] 171 | return opchoice(environ, start_response, CLIENTS) 172 | 173 | 174 | if __name__ == '__main__': 175 | parser = argparse.ArgumentParser() 176 | parser.add_argument("-b", required=True, dest="base", 177 | help="base url of the service provider") 178 | parser.add_argument("-p", dest="port", default=8090, 179 | help="port of the service provider", type=int) 180 | parser.add_argument("--provider", dest="provider", 181 | help="issuer url to non web OpenID Connect provider") 182 | parser.add_argument(dest="config") 183 | args = parser.parse_args() 184 | conf = importlib.import_module(args.config) 185 | 186 | args.base = "{base}:{port}".format(base=args.base.rstrip("/"), port=args.port) 187 | if not args.base.endswith("/"): 188 | args.base += "/" 189 | 190 | conf.ME["redirect_uris"] = [ 191 | url.format(base=args.base) for url in conf.ME["redirect_uris"]] 192 | 193 | if args.provider: 194 | conf.CLIENTS["non-web-op"] = { 195 | "srv_discovery_url": args.provider, 196 | "client_info": conf.ME, 197 | "behaviour": conf.BEHAVIOUR 198 | } 199 | 200 | session_opts = { 201 | 'session.type': 'memory', 202 | 'session.cookie_expires': True, 203 | 'session.auto': True, 204 | 'session.key': "{}.beaker.session.id".format( 205 | urlparse.urlparse(args.base).netloc.replace(":", ".")) 206 | } 207 | 208 | CLIENTS = OIDCClients(conf) 209 | 210 | SRV = wsgiserver.CherryPyWSGIServer(('0.0.0.0', args.port), 211 | SessionMiddleware(application, 212 | session_opts)) 213 | DATABASE = PamDatabase(conf.PAM_DATABASE) 214 | 215 | if args.base.startswith("https"): 216 | from cherrypy.wsgiserver import ssl_pyopenssl 217 | 218 | SRV.ssl_adapter = ssl_pyopenssl.pyOpenSSLAdapter( 219 | conf.SERVER_CERT, conf.SERVER_KEY, conf.CA_BUNDLE) 220 | 221 | LOGGER.info("RP server starting listening on port:%s" % args.port) 222 | print "RP server starting listening on port:%s" % args.port 223 | try: 224 | SRV.start() 225 | except KeyboardInterrupt: 226 | SRV.stop() 227 | -------------------------------------------------------------------------------- /src/service_provider/static/bootstrap/css/bootstrap-theme.css: -------------------------------------------------------------------------------- 1 | .btn-default, 2 | .btn-primary, 3 | .btn-success, 4 | .btn-info, 5 | .btn-warning, 6 | .btn-danger { 7 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); 8 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075); 9 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075); 10 | } 11 | 12 | .btn-default:active, 13 | .btn-primary:active, 14 | .btn-success:active, 15 | .btn-info:active, 16 | .btn-warning:active, 17 | .btn-danger:active, 18 | .btn-default.active, 19 | .btn-primary.active, 20 | .btn-success.active, 21 | .btn-info.active, 22 | .btn-warning.active, 23 | .btn-danger.active { 24 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); 25 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); 26 | } 27 | 28 | .btn:active, 29 | .btn.active { 30 | background-image: none; 31 | } 32 | 33 | .btn-default { 34 | text-shadow: 0 1px 0 #fff; 35 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#ffffff), to(#e6e6e6)); 36 | background-image: -webkit-linear-gradient(top, #ffffff, 0%, #e6e6e6, 100%); 37 | background-image: -moz-linear-gradient(top, #ffffff 0%, #e6e6e6 100%); 38 | background-image: linear-gradient(to bottom, #ffffff 0%, #e6e6e6 100%); 39 | background-repeat: repeat-x; 40 | border-color: #e0e0e0; 41 | border-color: #ccc; 42 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0); 43 | } 44 | 45 | .btn-default:active, 46 | .btn-default.active { 47 | background-color: #e6e6e6; 48 | border-color: #e0e0e0; 49 | } 50 | 51 | .btn-primary { 52 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#428bca), to(#3071a9)); 53 | background-image: -webkit-linear-gradient(top, #428bca, 0%, #3071a9, 100%); 54 | background-image: -moz-linear-gradient(top, #428bca 0%, #3071a9 100%); 55 | background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%); 56 | background-repeat: repeat-x; 57 | border-color: #2d6ca2; 58 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0); 59 | } 60 | 61 | .btn-primary:active, 62 | .btn-primary.active { 63 | background-color: #3071a9; 64 | border-color: #2d6ca2; 65 | } 66 | 67 | .btn-success { 68 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#5cb85c), to(#449d44)); 69 | background-image: -webkit-linear-gradient(top, #5cb85c, 0%, #449d44, 100%); 70 | background-image: -moz-linear-gradient(top, #5cb85c 0%, #449d44 100%); 71 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); 72 | background-repeat: repeat-x; 73 | border-color: #419641; 74 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); 75 | } 76 | 77 | .btn-success:active, 78 | .btn-success.active { 79 | background-color: #449d44; 80 | border-color: #419641; 81 | } 82 | 83 | .btn-warning { 84 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#f0ad4e), to(#ec971f)); 85 | background-image: -webkit-linear-gradient(top, #f0ad4e, 0%, #ec971f, 100%); 86 | background-image: -moz-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 87 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); 88 | background-repeat: repeat-x; 89 | border-color: #eb9316; 90 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); 91 | } 92 | 93 | .btn-warning:active, 94 | .btn-warning.active { 95 | background-color: #ec971f; 96 | border-color: #eb9316; 97 | } 98 | 99 | .btn-danger { 100 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#d9534f), to(#c9302c)); 101 | background-image: -webkit-linear-gradient(top, #d9534f, 0%, #c9302c, 100%); 102 | background-image: -moz-linear-gradient(top, #d9534f 0%, #c9302c 100%); 103 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); 104 | background-repeat: repeat-x; 105 | border-color: #c12e2a; 106 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); 107 | } 108 | 109 | .btn-danger:active, 110 | .btn-danger.active { 111 | background-color: #c9302c; 112 | border-color: #c12e2a; 113 | } 114 | 115 | .btn-info { 116 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#5bc0de), to(#31b0d5)); 117 | background-image: -webkit-linear-gradient(top, #5bc0de, 0%, #31b0d5, 100%); 118 | background-image: -moz-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 119 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); 120 | background-repeat: repeat-x; 121 | border-color: #2aabd2; 122 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); 123 | } 124 | 125 | .btn-info:active, 126 | .btn-info.active { 127 | background-color: #31b0d5; 128 | border-color: #2aabd2; 129 | } 130 | 131 | .thumbnail, 132 | .img-thumbnail { 133 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); 134 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); 135 | } 136 | 137 | .dropdown-menu > li > a:hover, 138 | .dropdown-menu > li > a:focus, 139 | .dropdown-menu > .active > a, 140 | .dropdown-menu > .active > a:hover, 141 | .dropdown-menu > .active > a:focus { 142 | background-color: #357ebd; 143 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#428bca), to(#357ebd)); 144 | background-image: -webkit-linear-gradient(top, #428bca, 0%, #357ebd, 100%); 145 | background-image: -moz-linear-gradient(top, #428bca 0%, #357ebd 100%); 146 | background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); 147 | background-repeat: repeat-x; 148 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); 149 | } 150 | 151 | .navbar { 152 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#ffffff), to(#f8f8f8)); 153 | background-image: -webkit-linear-gradient(top, #ffffff, 0%, #f8f8f8, 100%); 154 | background-image: -moz-linear-gradient(top, #ffffff 0%, #f8f8f8 100%); 155 | background-image: linear-gradient(to bottom, #ffffff 0%, #f8f8f8 100%); 156 | background-repeat: repeat-x; 157 | border-radius: 4px; 158 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); 159 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075); 160 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075); 161 | } 162 | 163 | .navbar .navbar-nav > .active > a { 164 | background-color: #f8f8f8; 165 | } 166 | 167 | .navbar-brand, 168 | .navbar-nav > li > a { 169 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25); 170 | } 171 | 172 | .navbar-inverse { 173 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#3c3c3c), to(#222222)); 174 | background-image: -webkit-linear-gradient(top, #3c3c3c, 0%, #222222, 100%); 175 | background-image: -moz-linear-gradient(top, #3c3c3c 0%, #222222 100%); 176 | background-image: linear-gradient(to bottom, #3c3c3c 0%, #222222 100%); 177 | background-repeat: repeat-x; 178 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); 179 | } 180 | 181 | .navbar-inverse .navbar-nav > .active > a { 182 | background-color: #222222; 183 | } 184 | 185 | .navbar-inverse .navbar-brand, 186 | .navbar-inverse .navbar-nav > li > a { 187 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 188 | } 189 | 190 | .navbar-static-top, 191 | .navbar-fixed-top, 192 | .navbar-fixed-bottom { 193 | border-radius: 0; 194 | } 195 | 196 | .alert { 197 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2); 198 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05); 199 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05); 200 | } 201 | 202 | .alert-success { 203 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#dff0d8), to(#c8e5bc)); 204 | background-image: -webkit-linear-gradient(top, #dff0d8, 0%, #c8e5bc, 100%); 205 | background-image: -moz-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 206 | background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); 207 | background-repeat: repeat-x; 208 | border-color: #b2dba1; 209 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); 210 | } 211 | 212 | .alert-info { 213 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#d9edf7), to(#b9def0)); 214 | background-image: -webkit-linear-gradient(top, #d9edf7, 0%, #b9def0, 100%); 215 | background-image: -moz-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 216 | background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); 217 | background-repeat: repeat-x; 218 | border-color: #9acfea; 219 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); 220 | } 221 | 222 | .alert-warning { 223 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#fcf8e3), to(#f8efc0)); 224 | background-image: -webkit-linear-gradient(top, #fcf8e3, 0%, #f8efc0, 100%); 225 | background-image: -moz-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 226 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); 227 | background-repeat: repeat-x; 228 | border-color: #f5e79e; 229 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); 230 | } 231 | 232 | .alert-danger { 233 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#f2dede), to(#e7c3c3)); 234 | background-image: -webkit-linear-gradient(top, #f2dede, 0%, #e7c3c3, 100%); 235 | background-image: -moz-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 236 | background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); 237 | background-repeat: repeat-x; 238 | border-color: #dca7a7; 239 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); 240 | } 241 | 242 | .progress { 243 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#ebebeb), to(#f5f5f5)); 244 | background-image: -webkit-linear-gradient(top, #ebebeb, 0%, #f5f5f5, 100%); 245 | background-image: -moz-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 246 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); 247 | background-repeat: repeat-x; 248 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); 249 | } 250 | 251 | .progress-bar { 252 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#428bca), to(#3071a9)); 253 | background-image: -webkit-linear-gradient(top, #428bca, 0%, #3071a9, 100%); 254 | background-image: -moz-linear-gradient(top, #428bca 0%, #3071a9 100%); 255 | background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%); 256 | background-repeat: repeat-x; 257 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0); 258 | } 259 | 260 | .progress-bar-success { 261 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#5cb85c), to(#449d44)); 262 | background-image: -webkit-linear-gradient(top, #5cb85c, 0%, #449d44, 100%); 263 | background-image: -moz-linear-gradient(top, #5cb85c 0%, #449d44 100%); 264 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); 265 | background-repeat: repeat-x; 266 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); 267 | } 268 | 269 | .progress-bar-info { 270 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#5bc0de), to(#31b0d5)); 271 | background-image: -webkit-linear-gradient(top, #5bc0de, 0%, #31b0d5, 100%); 272 | background-image: -moz-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 273 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); 274 | background-repeat: repeat-x; 275 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); 276 | } 277 | 278 | .progress-bar-warning { 279 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#f0ad4e), to(#ec971f)); 280 | background-image: -webkit-linear-gradient(top, #f0ad4e, 0%, #ec971f, 100%); 281 | background-image: -moz-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 282 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); 283 | background-repeat: repeat-x; 284 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); 285 | } 286 | 287 | .progress-bar-danger { 288 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#d9534f), to(#c9302c)); 289 | background-image: -webkit-linear-gradient(top, #d9534f, 0%, #c9302c, 100%); 290 | background-image: -moz-linear-gradient(top, #d9534f 0%, #c9302c 100%); 291 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); 292 | background-repeat: repeat-x; 293 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); 294 | } 295 | 296 | .list-group { 297 | border-radius: 4px; 298 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); 299 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); 300 | } 301 | 302 | .list-group-item.active, 303 | .list-group-item.active:hover, 304 | .list-group-item.active:focus { 305 | text-shadow: 0 -1px 0 #3071a9; 306 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#428bca), to(#3278b3)); 307 | background-image: -webkit-linear-gradient(top, #428bca, 0%, #3278b3, 100%); 308 | background-image: -moz-linear-gradient(top, #428bca 0%, #3278b3 100%); 309 | background-image: linear-gradient(to bottom, #428bca 0%, #3278b3 100%); 310 | background-repeat: repeat-x; 311 | border-color: #3278b3; 312 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0); 313 | } 314 | 315 | .panel { 316 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 317 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 318 | } 319 | 320 | .panel-default > .panel-heading { 321 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#f5f5f5), to(#e8e8e8)); 322 | background-image: -webkit-linear-gradient(top, #f5f5f5, 0%, #e8e8e8, 100%); 323 | background-image: -moz-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 324 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 325 | background-repeat: repeat-x; 326 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 327 | } 328 | 329 | .panel-primary > .panel-heading { 330 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#428bca), to(#357ebd)); 331 | background-image: -webkit-linear-gradient(top, #428bca, 0%, #357ebd, 100%); 332 | background-image: -moz-linear-gradient(top, #428bca 0%, #357ebd 100%); 333 | background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); 334 | background-repeat: repeat-x; 335 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); 336 | } 337 | 338 | .panel-success > .panel-heading { 339 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#dff0d8), to(#d0e9c6)); 340 | background-image: -webkit-linear-gradient(top, #dff0d8, 0%, #d0e9c6, 100%); 341 | background-image: -moz-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 342 | background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); 343 | background-repeat: repeat-x; 344 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); 345 | } 346 | 347 | .panel-info > .panel-heading { 348 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#d9edf7), to(#c4e3f3)); 349 | background-image: -webkit-linear-gradient(top, #d9edf7, 0%, #c4e3f3, 100%); 350 | background-image: -moz-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 351 | background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); 352 | background-repeat: repeat-x; 353 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); 354 | } 355 | 356 | .panel-warning > .panel-heading { 357 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#fcf8e3), to(#faf2cc)); 358 | background-image: -webkit-linear-gradient(top, #fcf8e3, 0%, #faf2cc, 100%); 359 | background-image: -moz-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 360 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); 361 | background-repeat: repeat-x; 362 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); 363 | } 364 | 365 | .panel-danger > .panel-heading { 366 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#f2dede), to(#ebcccc)); 367 | background-image: -webkit-linear-gradient(top, #f2dede, 0%, #ebcccc, 100%); 368 | background-image: -moz-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 369 | background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); 370 | background-repeat: repeat-x; 371 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); 372 | } 373 | 374 | .well { 375 | background-image: -webkit-gradient(linear, left 0%, left 100%, from(#e8e8e8), to(#f5f5f5)); 376 | background-image: -webkit-linear-gradient(top, #e8e8e8, 0%, #f5f5f5, 100%); 377 | background-image: -moz-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 378 | background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); 379 | background-repeat: repeat-x; 380 | border-color: #dcdcdc; 381 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); 382 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1); 383 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1); 384 | } -------------------------------------------------------------------------------- /src/service_provider/static/bootstrap/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | .btn-default, .btn-primary, .btn-success, .btn-info, .btn-warning, .btn-danger { 2 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); 3 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075); 4 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075) 5 | } 6 | 7 | .btn-default:active, .btn-primary:active, .btn-success:active, .btn-info:active, .btn-warning:active, .btn-danger:active, .btn-default.active, .btn-primary.active, .btn-success.active, .btn-info.active, .btn-warning.active, .btn-danger.active { 8 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); 9 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125) 10 | } 11 | 12 | .btn:active, .btn.active { 13 | background-image: none 14 | } 15 | 16 | .btn-default { 17 | text-shadow: 0 1px 0 #fff; 18 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#fff), to(#e6e6e6)); 19 | background-image: -webkit-linear-gradient(top, #fff, 0%, #e6e6e6, 100%); 20 | background-image: -moz-linear-gradient(top, #fff 0, #e6e6e6 100%); 21 | background-image: linear-gradient(to bottom, #fff 0, #e6e6e6 100%); 22 | background-repeat: repeat-x; 23 | border-color: #e0e0e0; 24 | border-color: #ccc; 25 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0) 26 | } 27 | 28 | .btn-default:active, .btn-default.active { 29 | background-color: #e6e6e6; 30 | border-color: #e0e0e0 31 | } 32 | 33 | .btn-primary { 34 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#428bca), to(#3071a9)); 35 | background-image: -webkit-linear-gradient(top, #428bca, 0%, #3071a9, 100%); 36 | background-image: -moz-linear-gradient(top, #428bca 0, #3071a9 100%); 37 | background-image: linear-gradient(to bottom, #428bca 0, #3071a9 100%); 38 | background-repeat: repeat-x; 39 | border-color: #2d6ca2; 40 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0) 41 | } 42 | 43 | .btn-primary:active, .btn-primary.active { 44 | background-color: #3071a9; 45 | border-color: #2d6ca2 46 | } 47 | 48 | .btn-success { 49 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#5cb85c), to(#449d44)); 50 | background-image: -webkit-linear-gradient(top, #5cb85c, 0%, #449d44, 100%); 51 | background-image: -moz-linear-gradient(top, #5cb85c 0, #449d44 100%); 52 | background-image: linear-gradient(to bottom, #5cb85c 0, #449d44 100%); 53 | background-repeat: repeat-x; 54 | border-color: #419641; 55 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0) 56 | } 57 | 58 | .btn-success:active, .btn-success.active { 59 | background-color: #449d44; 60 | border-color: #419641 61 | } 62 | 63 | .btn-warning { 64 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#f0ad4e), to(#ec971f)); 65 | background-image: -webkit-linear-gradient(top, #f0ad4e, 0%, #ec971f, 100%); 66 | background-image: -moz-linear-gradient(top, #f0ad4e 0, #ec971f 100%); 67 | background-image: linear-gradient(to bottom, #f0ad4e 0, #ec971f 100%); 68 | background-repeat: repeat-x; 69 | border-color: #eb9316; 70 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0) 71 | } 72 | 73 | .btn-warning:active, .btn-warning.active { 74 | background-color: #ec971f; 75 | border-color: #eb9316 76 | } 77 | 78 | .btn-danger { 79 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#d9534f), to(#c9302c)); 80 | background-image: -webkit-linear-gradient(top, #d9534f, 0%, #c9302c, 100%); 81 | background-image: -moz-linear-gradient(top, #d9534f 0, #c9302c 100%); 82 | background-image: linear-gradient(to bottom, #d9534f 0, #c9302c 100%); 83 | background-repeat: repeat-x; 84 | border-color: #c12e2a; 85 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0) 86 | } 87 | 88 | .btn-danger:active, .btn-danger.active { 89 | background-color: #c9302c; 90 | border-color: #c12e2a 91 | } 92 | 93 | .btn-info { 94 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#5bc0de), to(#31b0d5)); 95 | background-image: -webkit-linear-gradient(top, #5bc0de, 0%, #31b0d5, 100%); 96 | background-image: -moz-linear-gradient(top, #5bc0de 0, #31b0d5 100%); 97 | background-image: linear-gradient(to bottom, #5bc0de 0, #31b0d5 100%); 98 | background-repeat: repeat-x; 99 | border-color: #2aabd2; 100 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0) 101 | } 102 | 103 | .btn-info:active, .btn-info.active { 104 | background-color: #31b0d5; 105 | border-color: #2aabd2 106 | } 107 | 108 | .thumbnail, .img-thumbnail { 109 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); 110 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075) 111 | } 112 | 113 | .dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus, .dropdown-menu > .active > a, .dropdown-menu > .active > a:hover, .dropdown-menu > .active > a:focus { 114 | background-color: #357ebd; 115 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#428bca), to(#357ebd)); 116 | background-image: -webkit-linear-gradient(top, #428bca, 0%, #357ebd, 100%); 117 | background-image: -moz-linear-gradient(top, #428bca 0, #357ebd 100%); 118 | background-image: linear-gradient(to bottom, #428bca 0, #357ebd 100%); 119 | background-repeat: repeat-x; 120 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0) 121 | } 122 | 123 | .navbar { 124 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#fff), to(#f8f8f8)); 125 | background-image: -webkit-linear-gradient(top, #fff, 0%, #f8f8f8, 100%); 126 | background-image: -moz-linear-gradient(top, #fff 0, #f8f8f8 100%); 127 | background-image: linear-gradient(to bottom, #fff 0, #f8f8f8 100%); 128 | background-repeat: repeat-x; 129 | border-radius: 4px; 130 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); 131 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075); 132 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075) 133 | } 134 | 135 | .navbar .navbar-nav > .active > a { 136 | background-color: #f8f8f8 137 | } 138 | 139 | .navbar-brand, .navbar-nav > li > a { 140 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25) 141 | } 142 | 143 | .navbar-inverse { 144 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#3c3c3c), to(#222)); 145 | background-image: -webkit-linear-gradient(top, #3c3c3c, 0%, #222, 100%); 146 | background-image: -moz-linear-gradient(top, #3c3c3c 0, #222 100%); 147 | background-image: linear-gradient(to bottom, #3c3c3c 0, #222 100%); 148 | background-repeat: repeat-x; 149 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0) 150 | } 151 | 152 | .navbar-inverse .navbar-nav > .active > a { 153 | background-color: #222 154 | } 155 | 156 | .navbar-inverse .navbar-brand, .navbar-inverse .navbar-nav > li > a { 157 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25) 158 | } 159 | 160 | .navbar-static-top, .navbar-fixed-top, .navbar-fixed-bottom { 161 | border-radius: 0 162 | } 163 | 164 | .alert { 165 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2); 166 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05); 167 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05) 168 | } 169 | 170 | .alert-success { 171 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#dff0d8), to(#c8e5bc)); 172 | background-image: -webkit-linear-gradient(top, #dff0d8, 0%, #c8e5bc, 100%); 173 | background-image: -moz-linear-gradient(top, #dff0d8 0, #c8e5bc 100%); 174 | background-image: linear-gradient(to bottom, #dff0d8 0, #c8e5bc 100%); 175 | background-repeat: repeat-x; 176 | border-color: #b2dba1; 177 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0) 178 | } 179 | 180 | .alert-info { 181 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#d9edf7), to(#b9def0)); 182 | background-image: -webkit-linear-gradient(top, #d9edf7, 0%, #b9def0, 100%); 183 | background-image: -moz-linear-gradient(top, #d9edf7 0, #b9def0 100%); 184 | background-image: linear-gradient(to bottom, #d9edf7 0, #b9def0 100%); 185 | background-repeat: repeat-x; 186 | border-color: #9acfea; 187 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0) 188 | } 189 | 190 | .alert-warning { 191 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#fcf8e3), to(#f8efc0)); 192 | background-image: -webkit-linear-gradient(top, #fcf8e3, 0%, #f8efc0, 100%); 193 | background-image: -moz-linear-gradient(top, #fcf8e3 0, #f8efc0 100%); 194 | background-image: linear-gradient(to bottom, #fcf8e3 0, #f8efc0 100%); 195 | background-repeat: repeat-x; 196 | border-color: #f5e79e; 197 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0) 198 | } 199 | 200 | .alert-danger { 201 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#f2dede), to(#e7c3c3)); 202 | background-image: -webkit-linear-gradient(top, #f2dede, 0%, #e7c3c3, 100%); 203 | background-image: -moz-linear-gradient(top, #f2dede 0, #e7c3c3 100%); 204 | background-image: linear-gradient(to bottom, #f2dede 0, #e7c3c3 100%); 205 | background-repeat: repeat-x; 206 | border-color: #dca7a7; 207 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0) 208 | } 209 | 210 | .progress { 211 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#ebebeb), to(#f5f5f5)); 212 | background-image: -webkit-linear-gradient(top, #ebebeb, 0%, #f5f5f5, 100%); 213 | background-image: -moz-linear-gradient(top, #ebebeb 0, #f5f5f5 100%); 214 | background-image: linear-gradient(to bottom, #ebebeb 0, #f5f5f5 100%); 215 | background-repeat: repeat-x; 216 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0) 217 | } 218 | 219 | .progress-bar { 220 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#428bca), to(#3071a9)); 221 | background-image: -webkit-linear-gradient(top, #428bca, 0%, #3071a9, 100%); 222 | background-image: -moz-linear-gradient(top, #428bca 0, #3071a9 100%); 223 | background-image: linear-gradient(to bottom, #428bca 0, #3071a9 100%); 224 | background-repeat: repeat-x; 225 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0) 226 | } 227 | 228 | .progress-bar-success { 229 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#5cb85c), to(#449d44)); 230 | background-image: -webkit-linear-gradient(top, #5cb85c, 0%, #449d44, 100%); 231 | background-image: -moz-linear-gradient(top, #5cb85c 0, #449d44 100%); 232 | background-image: linear-gradient(to bottom, #5cb85c 0, #449d44 100%); 233 | background-repeat: repeat-x; 234 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0) 235 | } 236 | 237 | .progress-bar-info { 238 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#5bc0de), to(#31b0d5)); 239 | background-image: -webkit-linear-gradient(top, #5bc0de, 0%, #31b0d5, 100%); 240 | background-image: -moz-linear-gradient(top, #5bc0de 0, #31b0d5 100%); 241 | background-image: linear-gradient(to bottom, #5bc0de 0, #31b0d5 100%); 242 | background-repeat: repeat-x; 243 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0) 244 | } 245 | 246 | .progress-bar-warning { 247 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#f0ad4e), to(#ec971f)); 248 | background-image: -webkit-linear-gradient(top, #f0ad4e, 0%, #ec971f, 100%); 249 | background-image: -moz-linear-gradient(top, #f0ad4e 0, #ec971f 100%); 250 | background-image: linear-gradient(to bottom, #f0ad4e 0, #ec971f 100%); 251 | background-repeat: repeat-x; 252 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0) 253 | } 254 | 255 | .progress-bar-danger { 256 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#d9534f), to(#c9302c)); 257 | background-image: -webkit-linear-gradient(top, #d9534f, 0%, #c9302c, 100%); 258 | background-image: -moz-linear-gradient(top, #d9534f 0, #c9302c 100%); 259 | background-image: linear-gradient(to bottom, #d9534f 0, #c9302c 100%); 260 | background-repeat: repeat-x; 261 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0) 262 | } 263 | 264 | .list-group { 265 | border-radius: 4px; 266 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); 267 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075) 268 | } 269 | 270 | .list-group-item.active, .list-group-item.active:hover, .list-group-item.active:focus { 271 | text-shadow: 0 -1px 0 #3071a9; 272 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#428bca), to(#3278b3)); 273 | background-image: -webkit-linear-gradient(top, #428bca, 0%, #3278b3, 100%); 274 | background-image: -moz-linear-gradient(top, #428bca 0, #3278b3 100%); 275 | background-image: linear-gradient(to bottom, #428bca 0, #3278b3 100%); 276 | background-repeat: repeat-x; 277 | border-color: #3278b3; 278 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0) 279 | } 280 | 281 | .panel { 282 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 283 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) 284 | } 285 | 286 | .panel-default > .panel-heading { 287 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#f5f5f5), to(#e8e8e8)); 288 | background-image: -webkit-linear-gradient(top, #f5f5f5, 0%, #e8e8e8, 100%); 289 | background-image: -moz-linear-gradient(top, #f5f5f5 0, #e8e8e8 100%); 290 | background-image: linear-gradient(to bottom, #f5f5f5 0, #e8e8e8 100%); 291 | background-repeat: repeat-x; 292 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0) 293 | } 294 | 295 | .panel-primary > .panel-heading { 296 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#428bca), to(#357ebd)); 297 | background-image: -webkit-linear-gradient(top, #428bca, 0%, #357ebd, 100%); 298 | background-image: -moz-linear-gradient(top, #428bca 0, #357ebd 100%); 299 | background-image: linear-gradient(to bottom, #428bca 0, #357ebd 100%); 300 | background-repeat: repeat-x; 301 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0) 302 | } 303 | 304 | .panel-success > .panel-heading { 305 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#dff0d8), to(#d0e9c6)); 306 | background-image: -webkit-linear-gradient(top, #dff0d8, 0%, #d0e9c6, 100%); 307 | background-image: -moz-linear-gradient(top, #dff0d8 0, #d0e9c6 100%); 308 | background-image: linear-gradient(to bottom, #dff0d8 0, #d0e9c6 100%); 309 | background-repeat: repeat-x; 310 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0) 311 | } 312 | 313 | .panel-info > .panel-heading { 314 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#d9edf7), to(#c4e3f3)); 315 | background-image: -webkit-linear-gradient(top, #d9edf7, 0%, #c4e3f3, 100%); 316 | background-image: -moz-linear-gradient(top, #d9edf7 0, #c4e3f3 100%); 317 | background-image: linear-gradient(to bottom, #d9edf7 0, #c4e3f3 100%); 318 | background-repeat: repeat-x; 319 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0) 320 | } 321 | 322 | .panel-warning > .panel-heading { 323 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#fcf8e3), to(#faf2cc)); 324 | background-image: -webkit-linear-gradient(top, #fcf8e3, 0%, #faf2cc, 100%); 325 | background-image: -moz-linear-gradient(top, #fcf8e3 0, #faf2cc 100%); 326 | background-image: linear-gradient(to bottom, #fcf8e3 0, #faf2cc 100%); 327 | background-repeat: repeat-x; 328 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0) 329 | } 330 | 331 | .panel-danger > .panel-heading { 332 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#f2dede), to(#ebcccc)); 333 | background-image: -webkit-linear-gradient(top, #f2dede, 0%, #ebcccc, 100%); 334 | background-image: -moz-linear-gradient(top, #f2dede 0, #ebcccc 100%); 335 | background-image: linear-gradient(to bottom, #f2dede 0, #ebcccc 100%); 336 | background-repeat: repeat-x; 337 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0) 338 | } 339 | 340 | .well { 341 | background-image: -webkit-gradient(linear, left 0, left 100%, from(#e8e8e8), to(#f5f5f5)); 342 | background-image: -webkit-linear-gradient(top, #e8e8e8, 0%, #f5f5f5, 100%); 343 | background-image: -moz-linear-gradient(top, #e8e8e8 0, #f5f5f5 100%); 344 | background-image: linear-gradient(to bottom, #e8e8e8 0, #f5f5f5 100%); 345 | background-repeat: repeat-x; 346 | border-color: #dcdcdc; 347 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); 348 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1); 349 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1) 350 | } -------------------------------------------------------------------------------- /src/service_provider/static/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/its-dirg/non-web-oidc/8f5406f7a3148fd6a057a0ee4fb0ca35892e0c9c/src/service_provider/static/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /src/service_provider/static/bootstrap/fonts/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | -------------------------------------------------------------------------------- /src/service_provider/static/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/its-dirg/non-web-oidc/8f5406f7a3148fd6a057a0ee4fb0ca35892e0c9c/src/service_provider/static/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /src/service_provider/static/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/its-dirg/non-web-oidc/8f5406f7a3148fd6a057a0ee4fb0ca35892e0c9c/src/service_provider/static/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /src/service_provider/static/bootstrap/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * bootstrap.js v3.0.0 by @fat and @mdo 3 | * Copyright 2013 Twitter Inc. 4 | * http://www.apache.org/licenses/LICENSE-2.0 5 | */ 6 | if (!jQuery)throw new Error("Bootstrap requires jQuery"); 7 | +function (a) { 8 | "use strict"; 9 | function b() { 10 | var a = document.createElement("bootstrap"), b = {WebkitTransition: "webkitTransitionEnd", MozTransition: "transitionend", OTransition: "oTransitionEnd otransitionend", transition: "transitionend"}; 11 | for (var c in b)if (void 0 !== a.style[c])return{end: b[c]} 12 | } 13 | 14 | a.fn.emulateTransitionEnd = function (b) { 15 | var c = !1, d = this; 16 | a(this).one(a.support.transition.end, function () { 17 | c = !0 18 | }); 19 | var e = function () { 20 | c || a(d).trigger(a.support.transition.end) 21 | }; 22 | return setTimeout(e, b), this 23 | }, a(function () { 24 | a.support.transition = b() 25 | }) 26 | }(window.jQuery), +function (a) { 27 | "use strict"; 28 | var b = '[data-dismiss="alert"]', c = function (c) { 29 | a(c).on("click", b, this.close) 30 | }; 31 | c.prototype.close = function (b) { 32 | function c() { 33 | f.trigger("closed.bs.alert").remove() 34 | } 35 | 36 | var d = a(this), e = d.attr("data-target"); 37 | e || (e = d.attr("href"), e = e && e.replace(/.*(?=#[^\s]*$)/, "")); 38 | var f = a(e); 39 | b && b.preventDefault(), f.length || (f = d.hasClass("alert") ? d : d.parent()), f.trigger(b = a.Event("close.bs.alert")), b.isDefaultPrevented() || (f.removeClass("in"), a.support.transition && f.hasClass("fade") ? f.one(a.support.transition.end, c).emulateTransitionEnd(150) : c()) 40 | }; 41 | var d = a.fn.alert; 42 | a.fn.alert = function (b) { 43 | return this.each(function () { 44 | var d = a(this), e = d.data("bs.alert"); 45 | e || d.data("bs.alert", e = new c(this)), "string" == typeof b && e[b].call(d) 46 | }) 47 | }, a.fn.alert.Constructor = c, a.fn.alert.noConflict = function () { 48 | return a.fn.alert = d, this 49 | }, a(document).on("click.bs.alert.data-api", b, c.prototype.close) 50 | }(window.jQuery), +function (a) { 51 | "use strict"; 52 | var b = function (c, d) { 53 | this.$element = a(c), this.options = a.extend({}, b.DEFAULTS, d) 54 | }; 55 | b.DEFAULTS = {loadingText: "loading..."}, b.prototype.setState = function (a) { 56 | var b = "disabled", c = this.$element, d = c.is("input") ? "val" : "html", e = c.data(); 57 | a += "Text", e.resetText || c.data("resetText", c[d]()), c[d](e[a] || this.options[a]), setTimeout(function () { 58 | "loadingText" == a ? c.addClass(b).attr(b, b) : c.removeClass(b).removeAttr(b) 59 | }, 0) 60 | }, b.prototype.toggle = function () { 61 | var a = this.$element.closest('[data-toggle="buttons"]'); 62 | if (a.length) { 63 | var b = this.$element.find("input").prop("checked", !this.$element.hasClass("active")).trigger("change"); 64 | "radio" === b.prop("type") && a.find(".active").removeClass("active") 65 | } 66 | this.$element.toggleClass("active") 67 | }; 68 | var c = a.fn.button; 69 | a.fn.button = function (c) { 70 | return this.each(function () { 71 | var d = a(this), e = d.data("bs.button"), f = "object" == typeof c && c; 72 | e || d.data("bs.button", e = new b(this, f)), "toggle" == c ? e.toggle() : c && e.setState(c) 73 | }) 74 | }, a.fn.button.Constructor = b, a.fn.button.noConflict = function () { 75 | return a.fn.button = c, this 76 | }, a(document).on("click.bs.button.data-api", "[data-toggle^=button]", function (b) { 77 | var c = a(b.target); 78 | c.hasClass("btn") || (c = c.closest(".btn")), c.button("toggle"), b.preventDefault() 79 | }) 80 | }(window.jQuery), +function (a) { 81 | "use strict"; 82 | var b = function (b, c) { 83 | this.$element = a(b), this.$indicators = this.$element.find(".carousel-indicators"), this.options = c, this.paused = this.sliding = this.interval = this.$active = this.$items = null, "hover" == this.options.pause && this.$element.on("mouseenter", a.proxy(this.pause, this)).on("mouseleave", a.proxy(this.cycle, this)) 84 | }; 85 | b.DEFAULTS = {interval: 5e3, pause: "hover", wrap: !0}, b.prototype.cycle = function (b) { 86 | return b || (this.paused = !1), this.interval && clearInterval(this.interval), this.options.interval && !this.paused && (this.interval = setInterval(a.proxy(this.next, this), this.options.interval)), this 87 | }, b.prototype.getActiveIndex = function () { 88 | return this.$active = this.$element.find(".item.active"), this.$items = this.$active.parent().children(), this.$items.index(this.$active) 89 | }, b.prototype.to = function (b) { 90 | var c = this, d = this.getActiveIndex(); 91 | return b > this.$items.length - 1 || 0 > b ? void 0 : this.sliding ? this.$element.one("slid", function () { 92 | c.to(b) 93 | }) : d == b ? this.pause().cycle() : this.slide(b > d ? "next" : "prev", a(this.$items[b])) 94 | }, b.prototype.pause = function (b) { 95 | return b || (this.paused = !0), this.$element.find(".next, .prev").length && a.support.transition.end && (this.$element.trigger(a.support.transition.end), this.cycle(!0)), this.interval = clearInterval(this.interval), this 96 | }, b.prototype.next = function () { 97 | return this.sliding ? void 0 : this.slide("next") 98 | }, b.prototype.prev = function () { 99 | return this.sliding ? void 0 : this.slide("prev") 100 | }, b.prototype.slide = function (b, c) { 101 | var d = this.$element.find(".item.active"), e = c || d[b](), f = this.interval, g = "next" == b ? "left" : "right", h = "next" == b ? "first" : "last", i = this; 102 | if (!e.length) { 103 | if (!this.options.wrap)return; 104 | e = this.$element.find(".item")[h]() 105 | } 106 | this.sliding = !0, f && this.pause(); 107 | var j = a.Event("slide.bs.carousel", {relatedTarget: e[0], direction: g}); 108 | if (!e.hasClass("active")) { 109 | if (this.$indicators.length && (this.$indicators.find(".active").removeClass("active"), this.$element.one("slid", function () { 110 | var b = a(i.$indicators.children()[i.getActiveIndex()]); 111 | b && b.addClass("active") 112 | })), a.support.transition && this.$element.hasClass("slide")) { 113 | if (this.$element.trigger(j), j.isDefaultPrevented())return; 114 | e.addClass(b), e[0].offsetWidth, d.addClass(g), e.addClass(g), d.one(a.support.transition.end, function () { 115 | e.removeClass([b, g].join(" ")).addClass("active"), d.removeClass(["active", g].join(" ")), i.sliding = !1, setTimeout(function () { 116 | i.$element.trigger("slid") 117 | }, 0) 118 | }).emulateTransitionEnd(600) 119 | } else { 120 | if (this.$element.trigger(j), j.isDefaultPrevented())return; 121 | d.removeClass("active"), e.addClass("active"), this.sliding = !1, this.$element.trigger("slid") 122 | } 123 | return f && this.cycle(), this 124 | } 125 | }; 126 | var c = a.fn.carousel; 127 | a.fn.carousel = function (c) { 128 | return this.each(function () { 129 | var d = a(this), e = d.data("bs.carousel"), f = a.extend({}, b.DEFAULTS, d.data(), "object" == typeof c && c), g = "string" == typeof c ? c : f.slide; 130 | e || d.data("bs.carousel", e = new b(this, f)), "number" == typeof c ? e.to(c) : g ? e[g]() : f.interval && e.pause().cycle() 131 | }) 132 | }, a.fn.carousel.Constructor = b, a.fn.carousel.noConflict = function () { 133 | return a.fn.carousel = c, this 134 | }, a(document).on("click.bs.carousel.data-api", "[data-slide], [data-slide-to]", function (b) { 135 | var c, d = a(this), e = a(d.attr("data-target") || (c = d.attr("href")) && c.replace(/.*(?=#[^\s]+$)/, "")), f = a.extend({}, e.data(), d.data()), g = d.attr("data-slide-to"); 136 | g && (f.interval = !1), e.carousel(f), (g = d.attr("data-slide-to")) && e.data("bs.carousel").to(g), b.preventDefault() 137 | }), a(window).on("load", function () { 138 | a('[data-ride="carousel"]').each(function () { 139 | var b = a(this); 140 | b.carousel(b.data()) 141 | }) 142 | }) 143 | }(window.jQuery), +function (a) { 144 | "use strict"; 145 | var b = function (c, d) { 146 | this.$element = a(c), this.options = a.extend({}, b.DEFAULTS, d), this.transitioning = null, this.options.parent && (this.$parent = a(this.options.parent)), this.options.toggle && this.toggle() 147 | }; 148 | b.DEFAULTS = {toggle: !0}, b.prototype.dimension = function () { 149 | var a = this.$element.hasClass("width"); 150 | return a ? "width" : "height" 151 | }, b.prototype.show = function () { 152 | if (!this.transitioning && !this.$element.hasClass("in")) { 153 | var b = a.Event("show.bs.collapse"); 154 | if (this.$element.trigger(b), !b.isDefaultPrevented()) { 155 | var c = this.$parent && this.$parent.find("> .panel > .in"); 156 | if (c && c.length) { 157 | var d = c.data("bs.collapse"); 158 | if (d && d.transitioning)return; 159 | c.collapse("hide"), d || c.data("bs.collapse", null) 160 | } 161 | var e = this.dimension(); 162 | this.$element.removeClass("collapse").addClass("collapsing")[e](0), this.transitioning = 1; 163 | var f = function () { 164 | this.$element.removeClass("collapsing").addClass("in")[e]("auto"), this.transitioning = 0, this.$element.trigger("shown.bs.collapse") 165 | }; 166 | if (!a.support.transition)return f.call(this); 167 | var g = a.camelCase(["scroll", e].join("-")); 168 | this.$element.one(a.support.transition.end, a.proxy(f, this)).emulateTransitionEnd(350)[e](this.$element[0][g]) 169 | } 170 | } 171 | }, b.prototype.hide = function () { 172 | if (!this.transitioning && this.$element.hasClass("in")) { 173 | var b = a.Event("hide.bs.collapse"); 174 | if (this.$element.trigger(b), !b.isDefaultPrevented()) { 175 | var c = this.dimension(); 176 | this.$element[c](this.$element[c]())[0].offsetHeight, this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"), this.transitioning = 1; 177 | var d = function () { 178 | this.transitioning = 0, this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse") 179 | }; 180 | return a.support.transition ? (this.$element[c](0).one(a.support.transition.end, a.proxy(d, this)).emulateTransitionEnd(350), void 0) : d.call(this) 181 | } 182 | } 183 | }, b.prototype.toggle = function () { 184 | this[this.$element.hasClass("in") ? "hide" : "show"]() 185 | }; 186 | var c = a.fn.collapse; 187 | a.fn.collapse = function (c) { 188 | return this.each(function () { 189 | var d = a(this), e = d.data("bs.collapse"), f = a.extend({}, b.DEFAULTS, d.data(), "object" == typeof c && c); 190 | e || d.data("bs.collapse", e = new b(this, f)), "string" == typeof c && e[c]() 191 | }) 192 | }, a.fn.collapse.Constructor = b, a.fn.collapse.noConflict = function () { 193 | return a.fn.collapse = c, this 194 | }, a(document).on("click.bs.collapse.data-api", "[data-toggle=collapse]", function (b) { 195 | var c, d = a(this), e = d.attr("data-target") || b.preventDefault() || (c = d.attr("href")) && c.replace(/.*(?=#[^\s]+$)/, ""), f = a(e), g = f.data("bs.collapse"), h = g ? "toggle" : d.data(), i = d.attr("data-parent"), j = i && a(i); 196 | g && g.transitioning || (j && j.find('[data-toggle=collapse][data-parent="' + i + '"]').not(d).addClass("collapsed"), d[f.hasClass("in") ? "addClass" : "removeClass"]("collapsed")), f.collapse(h) 197 | }) 198 | }(window.jQuery), +function (a) { 199 | "use strict"; 200 | function b() { 201 | a(d).remove(), a(e).each(function (b) { 202 | var d = c(a(this)); 203 | d.hasClass("open") && (d.trigger(b = a.Event("hide.bs.dropdown")), b.isDefaultPrevented() || d.removeClass("open").trigger("hidden.bs.dropdown")) 204 | }) 205 | } 206 | 207 | function c(b) { 208 | var c = b.attr("data-target"); 209 | c || (c = b.attr("href"), c = c && /#/.test(c) && c.replace(/.*(?=#[^\s]*$)/, "")); 210 | var d = c && a(c); 211 | return d && d.length ? d : b.parent() 212 | } 213 | 214 | var d = ".dropdown-backdrop", e = "[data-toggle=dropdown]", f = function (b) { 215 | a(b).on("click.bs.dropdown", this.toggle) 216 | }; 217 | f.prototype.toggle = function (d) { 218 | var e = a(this); 219 | if (!e.is(".disabled, :disabled")) { 220 | var f = c(e), g = f.hasClass("open"); 221 | if (b(), !g) { 222 | if ("ontouchstart"in document.documentElement && !f.closest(".navbar-nav").length && a('