11"""
2- OIDC backend module.
2+ OIDC/OAuth2 backend module.
33"""
44import logging
55from datetime import datetime
66
7+ from idpyoidc .client .oauth2 .stand_alone_client import StandAloneClient
78from idpyoidc .server .user_authn .authn_context import UNSPECIFIED
89
910import satosa .logging_util as lu
1011from satosa .backends .base import BackendModule
11- from satosa .exception import SATOSAAuthenticationError
1212from satosa .internal import AuthenticationInformation
1313from satosa .internal import InternalData
14+ from ..exception import SATOSAAuthenticationError
15+ from ..response import Redirect
1416
1517logger = logging .getLogger (__name__ )
1618
17- """
18- OIDC/OAuth2 backend module.
19- """
20- from idpyoidc .client .oauth2 .stand_alone_client import StandAloneClient
21-
2219
2320class IdpyOIDCBackend (BackendModule ):
2421 """
2522 Backend module for OIDC and OAuth 2.0, can be directly used.
2623 """
2724
28- def __init__ (self , outgoing , internal_attributes , config , base_url , name ):
25+ def __init__ (self , auth_callback_func , internal_attributes , config , base_url , name ):
2926 """
30- :type outgoing:
27+ OIDC backend module.
28+ :param auth_callback_func: Callback should be called by the module after the authorization
29+ in the backend is done.
30+ :param internal_attributes: Mapping dictionary between SATOSA internal attribute names and
31+ the names returned by underlying IdP's/OP's as well as what attributes the calling SP's and
32+ RP's expects namevice.
33+ :param config: Configuration parameters for the module.
34+ :param base_url: base url of the service
35+ :param name: name of the plugin
36+
37+ :type auth_callback_func:
3138 (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response
32- :type internal_attributes: dict[str , dict[str, list[ str] | str]]
33- :type config: dict[str, Any ]
39+ :type internal_attributes: dict[string , dict[str, str | list[ str] ]]
40+ :type config: dict[str, dict[str, str] | list[str] ]
3441 :type base_url: str
3542 :type name: str
36-
37- :param outgoing: Callback should be called by the module after
38- the authorization in the backend is done.
39- :param internal_attributes: Internal attribute map
40- :param config: The module config
41- :param base_url: base url of the service
42- :param name: name of the plugin
4343 """
44- super ().__init__ (outgoing , internal_attributes , base_url , name )
45-
46- self .client = StandAloneClient (config = config ["client_config" ],
47- client_type = config ["client_config" ]['client_type' ])
48- # Deal with provider discovery and client registration
44+ super ().__init__ (auth_callback_func , internal_attributes , base_url , name )
45+ # self.auth_callback_func = auth_callback_func
46+ # self.config = config
47+ self .client = StandAloneClient (config = config ["client" ], client_type = "oidc" )
4948 self .client .do_provider_info ()
5049 self .client .do_client_registration ()
5150
@@ -57,7 +56,8 @@ def start_auth(self, context, internal_request):
5756 :type internal_request: satosa.internal.InternalData
5857 :rtype satosa.response.Redirect
5958 """
60- return self .client .init_authorization ()
59+ login_url = self .client .init_authorization ()
60+ return Redirect (login_url )
6161
6262 def register_endpoints (self ):
6363 """
@@ -67,8 +67,56 @@ def register_endpoints(self):
6767 :rtype: Sequence[(str, Callable[[satosa.context.Context], satosa.response.Response]]
6868 :return: A list that can be used to map the request to SATOSA to this endpoint.
6969 """
70+ return self .client .context .claims .get_usage ('redirect_uris' )
71+
72+ def response_endpoint (self , context , * args ):
73+ """
74+ Handles the authentication response from the OP.
75+ :type context: satosa.context.Context
76+ :type args: Any
77+ :rtype: satosa.response.Response
7078
71- return self .client .context .claims .get_usage ('authorization_endpoint' )
79+ :param context: SATOSA context
80+ :param args: None
81+ :return:
82+ """
83+
84+ _info = self .client .finalize (context .request )
85+ self ._check_error_response (_info , context )
86+ userinfo = _info .get ('userinfo' )
87+ id_token = _info .get ('id_token' )
88+
89+ if not id_token and not userinfo :
90+ msg = "No id_token or userinfo, nothing to do.."
91+ logline = lu .LOG_FMT .format (id = lu .get_session_id (context .state ), message = msg )
92+ logger .error (logline )
93+ raise SATOSAAuthenticationError (context .state , "No user info available." )
94+
95+ all_user_claims = dict (list (userinfo .items ()) + list (id_token .items ()))
96+ msg = "UserInfo: {}" .format (all_user_claims )
97+ logline = lu .LOG_FMT .format (id = lu .get_session_id (context .state ), message = msg )
98+ logger .debug (logline )
99+ internal_resp = self ._translate_response (all_user_claims , _info ["issuer" ])
100+ return self .auth_callback_func (context , internal_resp )
101+
102+ def _translate_response (self , response , issuer ):
103+ """
104+ Translates oidc response to SATOSA internal response.
105+ :type response: dict[str, str]
106+ :type issuer: str
107+ :type subject_type: str
108+ :rtype: InternalData
109+
110+ :param response: Dictioary with attribute name as key.
111+ :param issuer: The oidc op that gave the repsonse.
112+ :param subject_type: public or pairwise according to oidc standard.
113+ :return: A SATOSA internal response.
114+ """
115+ auth_info = AuthenticationInformation (UNSPECIFIED , str (datetime .now ()), issuer )
116+ internal_resp = InternalData (auth_info = auth_info )
117+ internal_resp .attributes = self .converter .to_internal ("openid" , response )
118+ internal_resp .subject_id = response ["sub" ]
119+ return internal_resp
72120
73121 def _check_error_response (self , response , context ):
74122 """
@@ -86,46 +134,3 @@ def _check_error_response(self, response, context):
86134 logline = lu .LOG_FMT .format (id = lu .get_session_id (context .state ), message = msg )
87135 logger .debug (logline )
88136 raise SATOSAAuthenticationError (context .state , "Access denied" )
89-
90- def _authn_response (self , context ):
91- """
92- Handles the authentication response from the AS.
93-
94- :type context: satosa.context.Context
95- :rtype: satosa.response.Response
96- :param context: The context in SATOSA
97- :return: A SATOSA response. This method is only responsible to call the callback function
98- which generates the Response object.
99- """
100-
101- _info = self .client .finalize (context .request )
102- self ._check_error_response (_info , context )
103-
104- try :
105- auth_info = self .auth_info (context .request )
106- except NotImplementedError :
107- auth_info = AuthenticationInformation (auth_class_ref = UNSPECIFIED ,
108- timestamp = str (datetime .now ()),
109- issuer = _info ["issuer" ])
110-
111- attributes = self .converter .to_internal (
112- self .client .client_type , _info ['userinfo' ],
113- )
114-
115- internal_response = InternalData (
116- auth_info = auth_info ,
117- attributes = attributes ,
118- subject_id = _info ['userinfo' ]['sub' ]
119- )
120- return internal_response
121-
122- def auth_info (self , request ):
123- """
124- Creates the SATOSA authentication information object.
125- :type request: dict[str, str]
126- :rtype: AuthenticationInformation
127-
128- :param request: The request parameters in the authentication response sent by the AS.
129- :return: How, who and when the authentication took place.
130- """
131- raise NotImplementedError ("Method 'auth_info' must be implemented in the subclass!" )
0 commit comments