この記事では、KerberosとJWTを使用したSSO認証の開発と展開について説明します。 認証モジュールは、Flask、Flask-Login、PyJWTを使用して開発されています。 展開は、CentOS 6/7でApache Webサーバー、FreeIPA認証サーバー、およびmod_lookup_identityモジュールを使用して行われました。 この記事には多くのテキスト、中程度のコード、いくつかの写真があります。 一般的に、それは面白いでしょう。
SSOについて少し説明します。 シングルサインオン(SSO)は、システムでの作業を開始するときに一度だけパスワードを入力できる認証原則であり、その後、すべてのドメインアプリケーションへのパスワードなしのアクセスをユーザーに提供します。 実際には、100%SSOは非常にまれです。これは、組織が単にそのような略語を知らないか、最新の方法をサポートしないレガシーシステムを持っていることが多いためです。 可能なSSOメソッドには、Kerberosプロトコル、SSL証明書などが含まれます。 実際、トークンの認証/検証のタスクは、各アプリケーションと中央認証サーバーの両方に割り当てることができます。 通常、SSOの実装は、ユーザーアカウントの中央データベースと、このデータベースを管理するソフトウェアの存在を意味します。
Windows環境の場合、SSOと集中ユーザーデータベースの両方を提供する標準ソリューションであるActive Directoryがあります。 Linuxの世界では、物事はそれほど単純ではありません。 NISも正常に死んでいました(完全ではありません)。LDAPには多くの「標準」ソリューションがあり、多く(および私も)はOpenLDAPでアドオンとWebインターフェースの一部を行い、winbindを使用してADと通信しようとしました。さらに。 私の謙虚な意見では、Red Hatは、FreeIPAを購入して追加した、Linuxの標準的な「ドメインコントローラー」の点で他の誰よりも遠い。 製品は1つのチームで展開され、RHEL / OEL / CentOS / Fedora環境で正常に動作し(Debianのクライアントモジュールもあると報告します)、ADでクロスドメイン認証を提供し、Webインターフェースを介して完全に制御され、DNS設定、自動マウント、 sudo ...要するに、私はそれを手に入れて、幸せに暮らしています。
それから繰り返しますが、ソフトウェアの書き方がよくわからず、ソフトウェアがあまり好きではないことを繰り返しますが、時々それが起こります。 だから私はGoogle Formsのキラーを書いた、そしてもちろん、タスクはユーザーを認証することでした。ケルベロスチケットをApacheにチェックするタスクを割り当て、その後、変数REMOTE_USERからのuidのLDAP(FreeIPAから)のデータを要求することで問題を解決しました 将来的には、 mod_lookup_identityを適用して、LDAPでの作業を拒否することさえできました。 しかし、このソリューションには弱点が1つありました-Windowsユーザーと私はFreeIPAによって管理されていないデバイスから来ており、したがってKerberosチケットを持っていません(厳密に言えば、勝利ユーザーはcmdを使用した倒錯またはADの展開とクロスを介してチケットを持つことができます-ドメインの信頼、しかしこれらの倒錯のいずれにも対処したくありませんでした。
むかしむかし、 JSON Web Tokensについて読み、私の手は常にそれらを試してみました。 そのため、この機会は現れました。 私はこれを行うことにしました。krbチケットを持っている人はKerberosで認証し、チケットを持っていない貧しい人はログインパスワードを入力して基本認証に進みます。 さらに、Basic Authにはmod_authnz_pamがあります 。これにより、パスワードを手動でチェックすることを完全に忘れることができます。 認証結果はCookieにJWTとして記録され、認証を要求したアプリケーションはトークンからこのデータを受け取ります。 したがって、JWTを発行する中央認証サービスのニーズが高まっています。
開発には、PythonとFlaskが使用されました(これが、多かれ少なかれ完全なアプリケーションを開発できる唯一の方法であるため)。 Flask-Loginは、PyJWTのFlaskで認証を管理し、jwtと連携するために使用されました 。 ソースへのリンクが必要な場合は、最後にあります。
妻のリクエストで、認証サービスはホグワーツの帽子(hh)と呼ばれました。その帽子はすべての人のすべてを知っていました。
hhの場合、独自のvirtualenvが作成され、コードがこのvirtualenvのルートにコピーされ、mod_wsgi上のアプリケーションが起動されます。 以下はApacheの設定です:
hogwartshat.conf
<VirtualHost *:80> ServerName hh.gsk.loc #WSGIプロセスパラメータ WSGIDaemonProcess hogwartshat user = hogwartshat group = hogwartshat threads = 10 WSGIScriptAlias / /var/www/flask/hogwartshat/hogwartshat.py WSGIScriptReloading On #認証パラメーター <場所/> AuthType Kerberos AuthName "HogwartsHat" #基本認証へのロールバックを有効にする KrbDelegateBasicオン KrbServiceName HTTP/garage.gsk.loc@GSK.LOC KrbMethodNegotiate On #次のディレクティブを無効にした場合-動作を停止した理由-理解できなかった KrbMethodK5Passwdオン KrbAuthRealms GSK.LOC Krb5KeyTab / etc / httpd / conf / keytab AuthBasicProvider PAM #/etc/pam.dからのPAM設定ファイルの表示 AuthPAMServiceガレージ 有効なユーザーが必要 #次のディレクティブは、DBusを介してsssdから取得したユーザー情報を環境変数に書き込みます LookupUserGECOS REMOTE_USER_FULLNAME LookupUserAttr uid REMOTE_USER_ID LookupUserAttr krbLastSuccessfulAuth REMOTE_USER_LASTGOODAUTH LookupUserAttr krbLastFailedAuth REMOTE_USER_LASTBADAUTH LookupUserGroups REMOTE_USER_GROUPS ":" #1秒(1000ミリ秒)未満のタイムアウトは意味がありません-DBusとLDAPには、20〜30%のケースで解決する時間がありません LookupDbusTimeout 2000 </場所> <ディレクトリ/ var / www / flask / hogwartshat> WSGIProcessGroup hogwartshat WSGIApplicationGroup%{GLOBAL} </ Directory> ログレベル警告 ErrorLogログ/ hogwartshat_error.log CustomLogログ/ hogwartshat_access.logの組み合わせ </ VirtualHost>
ロジックは次のとおりです。
- サーバーは最初のユーザー要求に401で応答し、ネゴシエート認証を要求します
- ユーザーがkrbチケットを提供します
- サーバーはsssdユーザー情報を要求し、環境変数を設定し、要求をwsgiアプリケーションに渡します
どちらか:
- サーバーは最初のユーザー要求に401で応答し、ネゴシエート認証を要求します
- ユーザーはkrbチケットを提供しません
- サーバーは401に応答し、基本認証を要求します
- ユーザーはログインパスワードを入力し、認証に成功します。
- サーバーはsssdユーザー情報を要求し、環境変数を設定し、要求をwsgiアプリケーションに渡します
それ以外の場合、ユーザーはサーバーから401を受け取ります。これはあまり良いことではありませんが、実装は簡単です。 別の方法はmod_intercept_form_submitですが、フォームを台無しにしたくありませんでした。
サービスwsgiファイルは次のようになります。
hogwartshat.py
#!/ usr / bin / env python #-*-コーディング:utf8-*- 輸入OS インポートシステム PROJECT_DIR = '/ var / www / flask / hogwartshat' #virtualenvのアクティベーション(実際、virtualenvを使用してPATHディレクトリの先頭に追加) activate_this = os.path.join(PROJECT_DIR、「bin」、「activate_this.py」) execfile(activate_this、dict(__ file __ = activate_this)) sys.path.append(PROJECT_DIR) アプリとしてアプリをアプリとしてインポート #in instance.py-暗号化キー application.config.from_object( 'app.config') application.config.from_pyfile( '../ instance.py')
__init__.pyはアプリパッケージにとって簡単なので、ここでは検討しません。 ただし、views.pyの方が興味深い-Flask-Loginを使用すると、ユーザーデータの操作が容易になります。
views.py、load_user_from_request()
@ login_manager.request_loader def load_user_from_request(req):logging.debug( 'req_loader env vars:%s'%str(req.environ))uid = req.environ.get( 'REMOTE_USER')if uid none:login_manager.login_mess =「ユーザーはHTTPDで認証されていません」 )、req.environ.get(app.config.get( 'HTTPD_UID_ATTR'))、req.environ.get(app.config.get( 'HTTPD_LAST_GOOD_AUTH_ATTR'))、req.environ.get(app.config.get( 'HTTPD_LAST_FAILED_AUTH_ATTR'))、req.environ.get(app.config.get( 'HTTPD_GROUPS_ATTR'))))AttributeError:を除く
主なアイデアはrequest_loaderで、Apacheによって設定された環境変数からHTTPDPoweredUser型のオブジェクトを作成します。 将来、login_requiredデコレータでラップされた関数では、current_user変数を介して情報とユーザーにアクセスできます。
サービスは、/認証済みユーザーにログインすると、次のように新しいjwt Cookieが発行されるように作成されます。
views.py、インデックス()
@ app.route( '/'、methods = ['GET']) @login_required defインデックス(): current_userがNoneでない場合: cookie = current_user.get_auth_token() expire_date = datetime.utcnow()+ timedelta(hours = app.config.get( 'JWT_EXPIRE_TIME_HOURS')) response = make_response(render_template( 'index.html'、user = current_user、cookie = cookie)) response.set_cookie( app.config.get( 'JWT_COOKIE_NAME')、 値= cookie、 expires = expire_date、 domain = app.config.get( 'JWT_COOKIE_DOMAIN')、 path = app.config.get( 'JWT_COOKIE_PATH')、 secure = app.config.get( 'SESSION_COOKIE_SECURE') ) logging.debug( 'jwt response:%s'%str(response)) 応答を返す その他: 中止(403)
users.py、get_auth_token()
def get_auth_token(self): トークン= { 'exp':datetime.utcnow()+ timedelta(時間= app.config.get( 'JWT_EXPIRE_TIME_HOURS'))、 'nbf':datetime.utcnow()、 「iss」:app.config.get(「JWT_ISSUER_NAME」)、 「aud」:app.config.get(「JWT_URN」)+「all」、 「uid」:self.uid、 'fullname':self.fullname、 「グループ」:self.groups } logging.debug( 'jwt tokens:%s'%str(tokens)) cookie = jwt.encode(トークン、app.config.get( 'JWT_PRIVATE_KEY')、アルゴリズム= app.config.get( 'JWT_ALG')) logging.debug( 'jwt cookie:%s'%str(cookie)) クッキーを返す
ご覧のとおり、uidに加えて、ユーザーとそのグループの名前もトークンに書き込まれているため、他のアプリケーションがユーザーに関する情報を得るために中央データベースにアクセスする必要がありません。
また、サービスには/ statusページがあり、jwtの状態を確認できます。
views.py、ステータス()
@ app.route( '/ status'、methods = ['GET']) @login_required defステータス(): auth_cookie = request.cookies.get(app.config.get( 'JWT_COOKIE_NAME')) logging.debug( 'cookie:%s'%str(auth_cookie)) トークン= {} error_message = '' auth_cookieがNoneでない場合: 試してください: トークン= jwt.decode( auth_cookie app.config.get( 'JWT_PUBLIC_KEY')、 オーディエンス= app.config.get( 'JWT_URN')+ 'all'、 発行者= app.config.get( 'JWT_ISSUER_NAME') ) nbf = datetime.utcfromtimestamp(tokens.get( 'nbf')) トークン['nbf'] = '(' + str(nbf)+ ')' + str(tokens.get( 'nbf')) exp = datetime.utcfromtimestamp(tokens.get( 'exp')) トークン['exp'] = '(' + str(exp)+ ')' + str(tokens.get( 'exp')) logging.debug(「Cookieが正常にデコードされました」) jwt.DecodeErrorを除く: logging.debug( 'status:jwt.DecodeError') error_message = '提供されたJWTのデコードに失敗しました' jwt.ExpiredSignatureErrorを除く: logging.debug( 'status:jwt.ExpiredSignatureError') error_message = 'JWTは期限切れです' jwt.InvalidIssuerErrorを除く: logging.debug( 'status:jwt.InvalidIssuerError') error_message = 'JWTは間違った発行者によって発行されました' jwt.InvalidAudienceErrorを除く: logging.debug( 'status:jwt.InvalidAudienceError') error_message =「JWTは別の対象者に対して発行されます」 その他: error_message = 'JWT Cookieを受信していません' logging.debug( 'tokens:%s'%str(tokens)) attr_error = current_userがNoneでない場合はfalse、そうでない場合はtrue render_template( 「status.html」、 error_message == ''の場合、error = false、true、 error_message = error_message、 トークン=トークン、 attr_error = attr_error、 ユーザー= current_user )
このようなキーを生成しました:
openssl ecparam -genkey -name secp521r1 -noout -out hogwartshat_key.pem#p521-タイプミスではない openssl ec -in hogwartshat_key.pem -pubout -out hogwartshat_pub.pem
次に、pemファイルの内容を構成にコピーしました。 PyJWTでは、非対称キーと楕円曲線を扱う暗号化モジュールが必要であることに注意してください。 私の手の曲率半径は、ドキュメントで提案されている代替モジュールでPyJWTを起動するには不十分でした。
実際、サードパーティアプリケーションの認証を担当するコードは次のとおりです。
views.py、return_to()
@ app.route( '/ return_to'、methods = ['GET']) @login_required def return_to(): app_id = request.args.get( 'appid') データ= request.args.get( 'data') app_idがNoneの場合: return make_error_page(「アプリケーションIDが指定されていません」、str(request.url))、400 elif app_idがapp.config.getにありません( 'APPS_PUBLIC_KEYS')。keys(): return make_error_page(「不明なアプリケーションIDが提供されました」、str(request.url))、403 データがNoneの場合: return make_error_page(「アプリケーションは空のリクエストを提供しました」、str(request.url))、400 その他: 試してください: トークン= jwt.decode( データ、 app.config.get( 'APPS_PUBLIC_KEYS')[app_id]、 オーディエンス= app.config.get( 'JWT_ISSUER_NAME')、 発行者= app.config.get( 'JWT_URN')+ app_id ) return_url = tokens.get( 'return_url') current_userがNoneでない場合: cookie = current_user.get_auth_token() expire_date = datetime.utcnow()+ timedelta(hours = app.config.get( 'JWT_EXPIRE_TIME_HOURS')) 応答= make_response(リダイレクト(str(return_url)、コード= 301)) response.set_cookie( app.config.get( 'JWT_COOKIE_NAME')、 値= cookie、 expires = expire_date、 domain = app.config.get( 'JWT_COOKIE_DOMAIN')、 path = app.config.get( 'JWT_COOKIE_PATH')、 secure = app.config.get( 'SESSION_COOKIE_SECURE') ) logging.debug( 'jwt response:%s'%str(response)) 応答を返す jwt.DecodeErrorを除く: return make_error_page(「提供されたJWTのデコードに失敗しました」、str(request.url))、412 jwt.ExpiredSignatureErrorを除く: return make_error_page( 'JWT is expired'、str(request.url))、412 jwt.InvalidIssuerErrorを除く: return make_error_page(「JWTは間違った発行者によって発行されました」、str(request.url))、412 jwt.InvalidAudienceErrorを除く: return make_error_page(「JWTは別のオーディエンスに対して発行されます」、str(request.url))、412 return str(request.args)
いくつかのスクリーンショット。 メインページ:
/ステータスページで確認できるように、Cookieは新鮮です。
ページ間の遷移によりkrbチケットを介したユーザー認証が発生するため、krb変数のlast_good_authが更新されました。 jwtでは、誰もCookieを更新しなかったため、expおよびnbfパラメーターは更新されませんでした。 しかし、Cookieが削除されるとどうなりますか:
さて、最も興味深いのは、サードパーティアプリケーションでの認証です。 実証するために、Cookieを読み取り、JWTからのデータページまたはエラーページを表示できる、小さくてandいアプリケーションを作成しました。 それは非常に小さくてthatいので、ここにすべてのコードを配置します。
デモ、__ init__.py
インポートjwt logging.configのインポート 日時インポート日時、timedeltaから フラスコからフラスコをインポート、リダイレクト、render_template、get_flashed_messages flask_loginからLoginManager、UserMixin、login_required、current_userをインポートします app = Flask(__ name__) app.config ['SECRET_KEY'] = '秘密鍵が設定されていないため、セッションは利用できません。 login_manager = LoginManager() login_manager.init_app(アプリ) キー= '' '----- ECプライベートキーの開始----- ----- ECプライベートキーの終了----- '' ' hh_pubkey = '' '-----公開キーの開始----- -----パブリックキーの終了----- '' ' logging.config.fileConfig( 'logging.conf') クラスJWTPoweredUser(UserMixin): def __init __(self、fullname、uid、groups): [フルネーム、uid、グループ]のattrの場合: attrがNoneの場合: AttributeErrorを発生させます( '%sをNoneにすることはできません'%attr .__ name__) self.fullname = fullname self.uid = uid self.groups =グループ def is_anonymous(self): 偽を返す def is_active(self): 真を返す def is_authenticated(self): 真を返す def get_id(自己): ユニコードを返す(self.uid) @ login_manager.request_loader def load_user_from_request(req): cookie = req.cookies.get( 'gsk_auth') CookieがNoneの場合: login_manager.login_message = 'Cookieなし' 何も返さない 試してください: トークン= jwt.decode(cookie、hh_pubkey、発行者= 'gsk:hogwartshat'、対象者= 'gsk:all') jwt.ExpiredSignatureErrorを除く: login_manager.login_message = '期限切れ' 何も返さない jwt.DecodeErrorを除く: login_manager.login_message = 'デコードエラー' 何も返さない jwt.InvalidIssuerErrorを除く: login_manager.login_message = '無効な発行者' 何も返さない jwt.InvalidAudienceErrorを除く: login_manager.login_message =「無効な対象者」 何も返さない return JWTPoweredUser(tokens.get( 'fullname')、tokens.get( 'uid')、tokens.get( 'groups')) @ login_manager.unauthorized_handler def無許可(): データ= jwt.encode({ 「iss」:「gsk:テスト」、 「aud」:「gsk:hogwartshat」、 'nbf':datetime.utcnow()、 'exp':datetime.utcnow()+ timedelta(分= 1)、 'return_url': 'http://jwttest.gsk.loc' }、キー、アルゴリズム= 'ES512') logging.debug( 'jwt request:%s'%data) url = 'http://hh.gsk.loc/return_to?appid = test&data =%s'%data logging.debug( 'jwt return_to:%s'%url) page = render_template( 「error.html」、 エラー= login_manager.login_message、 url = url ) logging.debug( 'jwt page:%s'%ページ) 戻りページ、403 @ app.route( '/'、methods = ['GET']) @login_required defインデックス(): render_template( 'index.html'、user = current_user)を返します
本質は同じです-カスタムrequest_loaderはトークンをチェックし、何か問題がある場合はNoneを返します。これにより、Flask-Loginは無許可のhandler_handlerを実行しますが、これもカスタムです。
Cookie無料デモ:
クッキーを探した後:
当然、403を表示する代わりに、リダイレクトを自動化することを禁止する人はいません。さらに、デモアプリケーションは元々そのように書かれていましたが、わかりやすくするために、画像ページはねじ込まれました。
古いものや不正なiss / audトークンを含むデータ要求パラメーターのごみを置き換えることで、オーセンティケーターをモックすることができます-彼は常にかみ砕いて誓います。 最後の未解決の問題が残っています-認証が必要なアプリケーションにエラーを報告する方法は? 現時点では、エラーレポートの送信先のリクエストでURLコールバックを渡すことが考えられます。 これまでのところ唯一の考えでしたので、私はそれを実装するために急いではありません。
2番目の未解決の問題はselinuxです。 暗号化モジュールはネイティブライブラリを使用するため、すべてlib_tタイプでマークする必要があります。 どうやら、私はまだそれを見つけられなかったので、今のところselinuxをオフにしました。 semanage fcontext -a -t <type> '<regex-path>'を使用してファイルのタイプ定義を追加します。
誰かが完全なソースコードに興味がある場合は、 こちらからダウンロードできます。 ライセンス-必要なことを行います。 コードがあなたにとって有用であれば-それは良いことです。
Scる。