def get_device(self, provider=None, user_code=None, client_id=None): """The device authorization endpoint can be used to request device and user codes. This endpoint is used to start the device flow authorization process and user code verification. User code confirmation:: GET LOCATION/device/<provider>?user_code=<user code> Response:: HTTP/1.1 200 OK """ if user_code: # If received a request with a user code, then prepare a request to authorization endpoint. self.log.verbose("User code verification.") result = self.server.db.getSessionByUserCode(user_code) if not result["OK"] or not result["Value"]: return getHTML( "session is expired.", theme="warning", body=result.get("Message"), info="Seems device code flow authorization session %s expired." % user_code, ) session = result["Value"] # Get original request from session req = createOAuth2Request(dict(method="GET", uri=session["uri"])) req.setQueryArguments(id=session["id"], user_code=user_code) # Save session to cookie and redirect to authorization endpoint authURL = "%s?%s" % (req.path.replace("device", "authorization"), req.query) return self.server.handle_response(302, {}, [("Location", authURL)], session) # If received a request without a user code, then send a form to enter the user code with dom.div(cls="row mt-5 justify-content-md-center") as tag: with dom.div(cls="col-auto"): dom.div( dom.form( dom._input(type="text", name="user_code"), dom.button("Submit", type="submit", cls="btn btn-submit"), action=self.currentPath, method="GET", ), cls="card", ) return getHTML( "user code verification..", body=tag, icon="ticket-alt", info="Device flow required user code. You will need to type user code to continue.", )
def prepare(self): self.set_status(404) from DIRAC.FrameworkSystem.private.authorization.utils.Utilities import getHTML self.finish( getHTML("Not found.", state=404, info="Nothing matches the given URI."))
def create_authorization_response(self, redirect_uri, user): """Mark session as authed with received user :param str redirect_uri: redirect uri :param dict user: dictionary with username and userID :return: result of `handle_response` """ result = self.server.db.getSessionByUserCode( self.request.data["user_code"]) if not result["OK"]: return 500, getHTML( "server error", theme="error", body=result["Message"], info="Failed to read %s authorization session." % self.request.data["user_code"], ) data = result["Value"] data.update( dict(user_id=user["ID"], uri=self.request.uri, username=user["username"], scope=self.request.scope)) # Save session with user result = self.server.db.updateSession(data, data["id"]) if not result["OK"]: return 500, getHTML( "server error", theme="error", body=result["Message"], info="Failed to save %s authorization session status." % self.request.data["user_code"], ) # Notify user that authorization completed. return 200, getHTML( "authorization complete!", theme="success", info= "Authorization has been completed, now you can close this window and return to the terminal.", )
def validate_consent_request(self, request, provider=None): """Validate current HTTP request for authorization page. This page is designed for resource owner to grant or deny the authorization:: :param object request: tornado request :param provider: provider :return: response generated by `handle_response` or S_ERROR or html """ try: request = self.create_oauth2_request(request) # Check Identity Provider req = self.validateIdentityProvider(request, provider) # If return HTML page with IdP selector if isinstance(req, str): return req sLog.info("Validate consent request for ", req.state) grant = self.get_authorization_grant(req) sLog.debug("Use grant:", grant.GRANT_TYPE) grant.validate_consent_request() if not hasattr(grant, "prompt"): grant.prompt = None # Submit second auth flow through IdP return self.getIdPAuthorization(req.provider, req) except OAuth2Error as error: self.db.removeSession(request.sessionID) code, body, _ = error(None) return self.handle_response( payload=getHTML(repr(error), state=code, body=body, info="OAuth2 error."), delSession=True ) except Exception as e: self.db.removeSession(request.sessionID) sLog.exception(e) return self.handle_response( payload=getHTML("server error", theme="error", body="traceback", info=repr(e)), delSession=True )
def parseIdPAuthorizationResponse(self, response, session): """Fill session by user profile, tokens, comment, OIDC authorize status, etc. Prepare dict with user parameters, if DN is absent there try to get it. Create new or modify existing DIRAC user and store the session :param dict response: authorization response :param str session: session :return: S_OK(dict)/S_ERROR() """ providerName = session.pop("Provider") sLog.debug("Try to parse authentification response from %s:\n" % providerName, pprint.pformat(response)) # Parse response result = self.idps.getIdProvider(providerName) if not result["OK"]: return result idpObj = result["Value"] result = idpObj.parseAuthResponse(response, session) if not result["OK"]: return result # FINISHING with IdP # As a result of authentication we will receive user credential dictionary credDict, payload = result["Value"] sLog.debug("Read profile:", pprint.pformat(credDict)) # Is ID registred? result = getUsernameForDN(credDict["DN"]) if not result["OK"]: comment = f"ID {credDict['ID']} is not registred in DIRAC. " payload.update(idpObj.getUserProfile().get("Value", {})) result = self.__registerNewUser(providerName, payload) if result["OK"]: comment += "Administrators have been notified about you." else: comment += "Please, contact the DIRAC administrators." # Notify user about problem html = getHTML("unregistered user!", info=comment, theme="warning") return S_ERROR(html) credDict["username"] = result["Value"] # Update token for user. This token will be stored separately in the database and # updated from time to time. This token will never be transmitted, # it will be used to make exchange token requests. result = self.tokenCli.updateToken(idpObj.token, credDict["ID"], idpObj.name) return S_OK(credDict) if result["OK"] else result
def create_authorization_response(self, response, username): """Rewrite original Authlib method `authlib.authlib.oauth2.rfc6749.authorization_server.create_authorization_response` to catch errors and remove authorization session. :return: TornadoResponse object """ try: response = super().create_authorization_response(response, username) response.clear_cookie("auth_session") return response except Exception as e: sLog.exception(e) return self.handle_response( payload=getHTML("server error", theme="error", body="traceback", info=repr(e)), delSession=True )
def __researchDIRACGroup(self, extSession, chooseScope, state): """Research DIRAC groups for authorized user :param dict extSession: ended authorized external IdP session :return: -- will return (None, response) to provide error or group selector will return (grant_user, request) to contionue authorization with choosed group """ # Base DIRAC client auth session firstRequest = createOAuth2Request(extSession["firstRequest"]) # Read requested groups by DIRAC client or user firstRequest.addScopes(chooseScope) # Read already authed user username = extSession["authed"]["username"] # Requested arguments in first request provider = firstRequest.provider self.log.debug("Next groups has been found for %s:" % username, ", ".join(firstRequest.groups)) # Researche Group result = getGroupsForUser(username) if not result["OK"]: return None, self.server.handle_response( payload=getHTML("server error", theme="error", info=result["Message"]), delSession=True ) groups = result["Value"] validGroups = [ group for group in groups if (getIdPForGroup(group) == provider) or ("proxy" in firstRequest.scope) ] if not validGroups: return None, self.server.handle_response( payload=getHTML( "groups not found.", theme="error", info=f"No groups found for {username} and for {provider} Identity Provider.", ), delSession=True, ) self.log.debug("The state of %s user groups has been checked:" % username, pprint.pformat(validGroups)) # If group already defined in first request, just return it if firstRequest.groups: return extSession["authed"], firstRequest # If not and we found only one valid group, apply this group if len(validGroups) == 1: firstRequest.addScopes(["g:%s" % validGroups[0]]) return extSession["authed"], firstRequest # Else give user chanse to choose group in browser with dom.div(cls="row mt-5 justify-content-md-center align-items-center") as tag: for group in sorted(validGroups): vo, gr = group.split("_") with dom.div(cls="col-auto p-2").add(dom.div(cls="card shadow-lg border-0 text-center p-2")): dom.h4(vo.upper() + " " + gr, cls="p-2") dom.a(href="%s?state=%s&chooseScope=g:%s" % (self.currentPath, state, group), cls="stretched-link") html = getHTML( "group selection..", body=tag, icon="users", info="Dirac use groups to describe permissions. " "You will need to select one of the groups to continue.", ) return None, self.server.handle_response(payload=html, newSession=extSession)
def get_redirect(self, state, error=None, error_description="", chooseScope=[]): """Redirect endpoint. After a user successfully authorizes an application, the authorization server will redirect the user back to the application with either an authorization code or access token in the URL. The full URL of this endpoint must be registered in the identity provider. Read more in `oauth.com <https://www.oauth.com/oauth2-servers/redirect-uris/>`_. Specified by `RFC6749 <https://tools.ietf.org/html/rfc6749#section-3.1.2>`_. GET LOCATION/redirect :param str state: Current IdP session state :param str error: IdP error response :param str error_description: error description :param list chooseScope: to specify new scope(group in our case) (optional) :return: S_OK()/S_ERROR() """ # Check current auth session that was initiated for the selected external identity provider session = self.get_secure_cookie("auth_session") if not session: return self.server.handle_response( payload=getHTML( "session is expired.", theme="warning", state=400, info="Seems %s session is expired, please, try again." % state, ), delSession=True, ) sessionWithExtIdP = json.loads(session) if state and not sessionWithExtIdP.get("state") == state: return self.server.handle_response( payload=getHTML( "session is expired.", theme="warning", state=400, info="Seems %s session is expired, please, try again." % state, ), delSession=True, ) # Try to catch errors if the authorization on the selected identity provider was unsuccessful if error: provider = sessionWithExtIdP.get("Provider") return self.server.handle_response( payload=getHTML( error, theme="error", body=error_description, info="Seems %s session is failed on the %s's' side." % (state, provider), ), delSession=True, ) if not sessionWithExtIdP.get("authed"): # Parse result of the second authentication flow self.log.info("%s session, parsing authorization response:\n" % state, self.request.uri) result = self.server.parseIdPAuthorizationResponse(self.request, sessionWithExtIdP) if not result["OK"]: if result["Message"].startswith("<!DOCTYPE html>"): return self.server.handle_response(payload=result["Message"], delSession=True) return self.server.handle_response( payload=getHTML("server error", state=500, info=result["Message"]), delSession=True ) # Return main session flow sessionWithExtIdP["authed"] = result["Value"] # Research group grant_user, response = self.__researchDIRACGroup(sessionWithExtIdP, chooseScope, state) if not grant_user: return response # RESPONSE to basic DIRAC client request resp = self.server.create_authorization_response(response, grant_user) if isinstance(resp.payload, str) and not resp.payload.startswith("<!DOCTYPE html>"): resp.payload = getHTML("authorization response", state=resp.status_code, body=resp.payload) return resp
def validateIdentityProvider(self, request, provider): """Check if identity provider registred in DIRAC :param object request: request :param str provider: provider name :return: OAuth2Request object or HTML -- new request with provider name or provider selector """ if provider: request.provider = provider # Find identity provider for group groupProvider = getIdPForGroup(request.group) if request.groups else None # If requested access token for group that is not registred in any identity provider # or the requested provider does not match the group return error if request.group and not groupProvider and "proxy" not in request.scope: raise Exception("The %s group belongs to the VO that is not tied to any Identity Provider." % request.group) sLog.debug("Check if %s identity provider registred in DIRAC.." % request.provider) # Research supported IdPs result = getProvidersForInstance("Id") if not result["OK"]: raise Exception(result["Message"]) idPs = result["Value"] if not idPs: raise Exception("No identity providers found.") if request.provider: if request.provider not in idPs: raise Exception("%s identity provider is not registered." % request.provider) elif groupProvider and request.provider != groupProvider: raise Exception( 'The %s group Identity Provider is "%s" and not "%s".' % (request.group, groupProvider, request.provider) ) return request # If no identity provider is specified, it must be assigned if groupProvider: request.provider = groupProvider return request # If only one identity provider is registered, then choose it if len(idPs) == 1: request.provider = idPs[0] return request # Choose IdP HTML interface with dom.div(cls="row m-5 justify-content-md-center") as tag: for idP in idPs: result = getProviderInfo(idP) if result["OK"]: logo = result["Value"].get("logoURL") with dom.div(cls="col-md-6 p-2").add(dom.div(cls="card shadow-lg h-100 border-0")): with dom.div(cls="row m-2 justify-content-md-center align-items-center h-100"): with dom.div(cls="col-auto"): dom.h2(idP) dom.a( href="%s/authorization/%s?%s" % (self.LOCATION, idP, request.query), cls="stretched-link", ) if logo: dom.div(dom.img(src=logo, cls="card-img"), cls="col-auto") # Render into HTML return getHTML( "Identity Provider selection..", body=tag, icon="fingerprint", info="Dirac itself is not an Identity Provider. " "You will need to select one to continue.", )