用 Identity Server 4 (JWKS 端點和 RS256 算法) 來保護 Python web api

原文: 用 Identity Server 4 (JWKS 端點和 RS256 算法) 來保護 Python web api

[新添加] 本文對應的源碼 (多個flow, clients, 調用python api): https://github.com/solenovex/Identity-Server-4-Python-Hug-Api-Jwkshtml

目前正在使用asp.net core 2.0 (主要是web api)作一個項目, 其中一部分功能須要使用js客戶端調用python的pandas, 因此須要創建一個python 的 rest api, 我暫時選用了hug, 官網在這: http://www.hug.rest/.node

目前項目使用的是identity server 4, 還有一些web api和js client.python

項目的早期後臺源碼: https://github.com/solenovex/asp.net-core-2.0-web-api-boilerplategit

下面開始配置identity server 4, 我使用的是windows.github

添加ApiResource:

在 authorization server項目中的配置文件添加紅色部分, 這部分就是python hug 的 api:web

public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
                new ApiResource(SalesApiSettings.ApiName, SalesApiSettings.ApiDisplayName) {
                    UserClaims = { JwtClaimTypes.Name, JwtClaimTypes.PreferredUserName, JwtClaimTypes.Email }
                },
                new ApiResource("purchaseapi", "採購和原料庫API") {
                    UserClaims = { JwtClaimTypes.Name, JwtClaimTypes.PreferredUserName, JwtClaimTypes.Email }
                },
                new ApiResource("hugapi", "Hug API") {
                    UserClaims = { JwtClaimTypes.Name, JwtClaimTypes.PreferredUserName, JwtClaimTypes.Email } }
            };
        }

修改js Client的配置:

// Sales JavaScript Client
                new Client
                {
                    ClientId = SalesApiSettings.ClientId,
                    ClientName = SalesApiSettings.ClientName,
                    AllowedGrantTypes = GrantTypes.Implicit,
                    AllowAccessTokensViaBrowser = true,
                    AccessTokenLifetime = 60 * 10,
                    AllowOfflineAccess = true,
                    RedirectUris =           { $"{Startup.Configuration["MLH:SalesApi:ClientBase"]}/login-callback", $"{Startup.Configuration["MLH:SalesApi:ClientBase"]}/silent-renew.html" },
                    PostLogoutRedirectUris = { Startup.Configuration["MLH:SalesApi:ClientBase"] },
                    AllowedCorsOrigins =     { Startup.Configuration["MLH:SalesApi:ClientBase"] },
                    AlwaysIncludeUserClaimsInIdToken = true,
                    AllowedScopes =
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        IdentityServerConstants.StandardScopes.Email,
                        SalesApiSettings.ApiName,
                        "hugapi"
                    }
                }

修改js客戶端的oidc client配置選項:

添加 hugapi, 與authorization server配置對應.算法

{
        authority: 'http://localhost:5000',
        client_id: 'sales',
        redirect_uri: 'http://localhost:4200/login-callback',
        response_type: 'id_token token',
        scope: 'openid profile salesapi hugapi email',
        post_logout_redirect_uri: 'http://localhost:4200',

        silent_redirect_uri: 'http://localhost:4200/silent-renew.html',
        automaticSilentRenew: true,
        accessTokenExpiringNotificationTime: 4,
        // silentRequestTimeout:10000,
        userStore: new WebStorageStateStore({ store: window.localStorage })
    }

創建Python Hug api

(可選) 安裝virtualenv:json

pip install virtualenv

而後在某個地方創建一個目錄:windows

mkdir hugapi && cd hugapi

創建虛擬環境:api

virtualenv venv

激活虛擬環境:

venv\Scripts\activate

而後大約這樣顯示:

安裝hug:

pip install hug

這時, 參考一下hug的文檔. 而後創建一個簡單的api. 創建文件main.py:

import hug

@hug.get('/home')
def root():
    return 'Welcome home!'

運行:

hug -f main.py

結果好用:

而後還須要安裝這些:

pip install cryptography pyjwt hug_middleware_cors

其中pyjwt是一個能夠encode和decode JWT的庫, 若是使用RS256算法的話, 還須要安裝cryptography. 

而hug_middleware_cors是hug的一個跨域訪問中間件(由於js客戶端和這個api不是在同一個域名下).

添加須要的引用:

import hug
import jwt
import json
import urllib.request
from jwt.algorithms import get_default_algorithms
from hug_middleware_cors import CORSMiddleware

而後正確的作法是經過Authorization Server的discovery endpoint來找到jwks_uri,

identity server 4 的discovery endpoint的地址是:

http://localhost:5000/.well-known/openid-configuration, 裏面能找到各類節點和信息:

 

但我仍是直接寫死這個jwks_uri吧:

response = urllib.request.urlopen('http://localhost:5000/.well-known/openid-configuration/jwks')
still_json = json.dumps(json.loads(response.read())['keys'][0])

identity server 4的jwks_uri, 裏面是public key, 它的結構是這樣的:

而我使用jwt庫, 的參數只能傳入一個證書的json, 也可就是keys[0].

因此上面的最後一行代碼顯得有點.......

若是使用python-jose這個庫會更簡單一些, 可是在我windows電腦上老是安裝失敗, 因此仍是湊合用pyjwt吧.

而後讓hug api使用cors中間件:

api = hug.API(__name__)
api.http.add_middleware(CORSMiddleware(api))

而後是hug的authentication部分:

def token_verify(token):
    token = token.replace('Bearer ', '')
    rsa = get_default_algorithms()['RS256']
    cert = rsa.from_jwk(still_json)
    try:
        result = jwt.decode(token, cert, algorithms=['RS256'], audience='hugapi')
        print(result)
        return result
    except jwt.DecodeError:
        return False

token_key_authentication = hug.authentication.token(token_verify)

經過rsa.from_jwk(json) 就會獲得key (certificate), 而後經過jwt.decode方法能夠把token進行驗證並decode, 算法是RS256, 這個方法要求若是token裏面包含了aud, 那麼方法就須要要指定audience, 也就是hugapi.

最後修改api 方法, 加上驗證:

@hug.get('/home', requires=token_key_authentication)
def root():
    return 'Welcome home!'

最後運行 hug api:

hug -f main.py

端口應該是8000.

運行js客戶端,登錄, 並調用這個hug api http://localhost:8000/home:

(個人js客戶端是angular5的, 這個無法開源, 公司財產, 不過配置oidc-client仍是很簡單的, 使用)

返回200, 內容是: 

看一下hug的log:

token被正確驗證並解析了. 因此能夠進入root方法了.

 
其餘的python api框架, 都是一樣的道理.

[新添加] 本文對應的源碼 (多個flow, clients, 調用python api): https://github.com/solenovex/Identity-Server-4-Python-Hug-Api-Jwks

可使用這個例子自行搭建 https://github.com/IdentityServer/IdentityServer4.Samples/tree/release/Quickstarts/7_JavaScriptClient 

官方還有一個nodejs api的例子: https://github.com/lyphtec/idsvr4-node-jwks

今日修改後的代碼: 

import json
import hug
import jwt
import requests
from jwt.algorithms import get_default_algorithms
from hug_middleware_cors import CORSMiddleware

api = hug.API(__name__)
api.http.add_middleware(CORSMiddleware(api))


def token_verify(token):
    access_token = token.replace('Bearer ', '')
    token_header = jwt.get_unverified_header(access_token)
    res = requests.get(
        'http://localhost:5000/.well-known/openid-configuration')
    jwk_uri = res.json()['jwks_uri']
    res = requests.get(jwk_uri)
    jwk_keys = res.json()

    rsa = get_default_algorithms()['RS256']
    key = json.dumps(jwk_keys['keys'][0])
    public_key = rsa.from_jwk(key)

    try:
        result = jwt.decode(access_token, public_key, algorithms=[
                            token_header['alg']], audience='api1')
        return result
    except jwt.DecodeError:
        return False


token_key_authentication = hug.authentication.token(token_verify)


@hug.get('/identity', requires=token_key_authentication)
def root(user: hug.directives.user):
    print(user)
    return user

 個人博客即將搬運同步至騰訊雲+社區,邀請你們一同入駐:https://cloud.tencent.com/developer/support-plan

相關文章
相關標籤/搜索