みなさんこんにちは。
この記事では、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る。