Beispiel #1
0
class FreeToPlayApiController(RedditController):
    @validate(item_name=VRequired('item', errors.NO_NAME),
              target=VByName('target'))
    def POST_use_item(self, item_name, target):
        try:
            inventory.consume_item(c.user, item_name)
        except inventory.NoSuchItemError:
            abort(400)

        c.user.f2p = "participated"
        c.user._commit()

        item = items.get_item(item_name)
        if not item.is_target_valid(target):
            abort(400)
        item.on_use(c.user, target)

        return json.dumps(c.state_changes)
Beispiel #2
0
class OAuth2FrontendController(RedditController):
    def check_for_bearer_token(self):
        pass

    def pre(self):
        RedditController.pre(self)
        require_https()

    def _check_redirect_uri(self, client, redirect_uri):
        if not redirect_uri or not client or redirect_uri != client.redirect_uri:
            abort(ForbiddenError(errors.OAUTH2_INVALID_REDIRECT_URI))

    def _error_response(self, state, redirect_uri, as_fragment=False):
        """Return an error redirect, but only if client_id and redirect_uri are valid."""

        resp = {"state": state}

        if (errors.OAUTH2_INVALID_CLIENT, "client_id") in c.errors:
            resp["error"] = "unauthorized_client"
        elif (errors.OAUTH2_ACCESS_DENIED, "authorize") in c.errors:
            resp["error"] = "access_denied"
        elif (errors.BAD_HASH, None) in c.errors:
            resp["error"] = "access_denied"
        elif (errors.INVALID_OPTION, "response_type") in c.errors:
            resp["error"] = "unsupported_response_type"
        elif (errors.OAUTH2_INVALID_SCOPE, "scope") in c.errors:
            resp["error"] = "invalid_scope"
        else:
            resp["error"] = "invalid_request"

        final_redirect = _update_redirect_uri(redirect_uri, resp, as_fragment)
        return self.redirect(final_redirect, code=302)

    @validate(VUser(),
              response_type=VOneOf("response_type", ("code", "token")),
              client=VOAuth2ClientID(),
              redirect_uri=VRequired("redirect_uri",
                                     errors.OAUTH2_INVALID_REDIRECT_URI),
              scope=VOAuth2Scope(),
              state=VRequired("state", errors.NO_TEXT),
              duration=VOneOf("duration", ("temporary", "permanent"),
                              default="temporary"))
    def GET_authorize(self, response_type, client, redirect_uri, scope, state,
                      duration):
        """
        First step in [OAuth 2.0](http://oauth.net/2/) authentication.
        End users will be prompted for their credentials (username/password)
        and asked if they wish to authorize the application identified by
        the **client_id** parameter with the permissions specified by the
        **scope** parameter.  They are then redirected to the endpoint on
        the client application's side specified by **redirect_uri**.

        If the user granted permission to the application, the response will
        contain a **code** parameter with a temporary authorization code
        which can be exchanged for an access token at
        [/api/v1/access_token](#api_method_access_token).

        **redirect_uri** must match the URI configured for the client in the
        [app preferences](/prefs/apps).  If **client_id** or **redirect_uri**
        is not valid, or if the call does not take place over SSL, a 403
        error will be returned.  For all other errors, a redirect to
        **redirect_uri** will be returned, with a **error** parameter
        indicating why the request failed.
        """

        # Check redirect URI first; it will ensure client exists
        self._check_redirect_uri(client, redirect_uri)

        if response_type == "token" and client.is_confidential():
            # Prevent "confidential" clients from distributing tokens
            # in a non-confidential manner
            c.errors.add((errors.OAUTH2_INVALID_CLIENT, "client_id"))
        if response_type == "token" and duration != "temporary":
            # implicit grant -> No refresh tokens allowed
            c.errors.add((errors.INVALID_OPTION, "duration"))

        if not c.errors:
            return OAuth2AuthorizationPage(client, redirect_uri, scope, state,
                                           duration, response_type).render()
        else:
            return self._error_response(state,
                                        redirect_uri,
                                        as_fragment=(response_type == "token"))

    @validate(VUser(),
              VModhash(fatal=False),
              client=VOAuth2ClientID(),
              redirect_uri=VRequired("redirect_uri",
                                     errors.OAUTH2_INVALID_REDIRECT_URI),
              scope=VOAuth2Scope(),
              state=VRequired("state", errors.NO_TEXT),
              duration=VOneOf("duration", ("temporary", "permanent"),
                              default="temporary"),
              authorize=VRequired("authorize", errors.OAUTH2_ACCESS_DENIED),
              response_type=VOneOf("response_type", ("code", "token"),
                                   default="code"))
    def POST_authorize(self, authorize, client, redirect_uri, scope, state,
                       duration, response_type):
        """Endpoint for OAuth2 authorization."""

        if response_type == "token" and client.is_confidential():
            # Prevent "confidential" clients from distributing tokens
            # in a non-confidential manner
            c.errors.add((errors.OAUTH2_INVALID_CLIENT, "client_id"))
        if response_type == "token" and duration != "temporary":
            # implicit grant -> No refresh tokens allowed
            c.errors.add((errors.INVALID_OPTION, "duration"))
        self._check_redirect_uri(client, redirect_uri)

        if c.errors:
            return self._error_response(state,
                                        redirect_uri,
                                        as_fragment=(response_type == "token"))

        if response_type == "code":
            code = OAuth2AuthorizationCode._new(client._id, redirect_uri,
                                                c.user._id36, scope,
                                                duration == "permanent")
            resp = {"code": code._id, "state": state}
            final_redirect = _update_redirect_uri(redirect_uri, resp)
        elif response_type == "token":
            token = OAuth2AccessToken._new(client._id, c.user._id36, scope)
            token_data = OAuth2AccessController._make_token_dict(token)
            token_data["state"] = state
            final_redirect = _update_redirect_uri(redirect_uri,
                                                  token_data,
                                                  as_fragment=True)

        return self.redirect(final_redirect, code=302)
Beispiel #3
0
class OAuth2AccessController(MinimalController):
    handles_csrf = True

    def pre(self):
        set_extension(request.environ, "json")
        MinimalController.pre(self)
        require_https()
        if request.method != "OPTIONS":
            c.oauth2_client = self._get_client_auth()

    def _get_client_auth(self):
        auth = request.headers.get("Authorization")
        try:
            client_id, client_secret = parse_http_basic(auth)
            require(client_id)
            client = OAuth2Client.get_token(client_id)
            require(client)
            if client.is_confidential():
                require(client_secret)
                require(constant_time_compare(client.secret, client_secret))
            return client
        except RequirementException:
            abort(401, headers=[("WWW-Authenticate", 'Basic realm="reddit"')])

    @validate(
        grant_type=VOneOf("grant_type", (
            "authorization_code",
            "refresh_token",
            "password",
            "client_credentials",
            "https://oauth.reddit.com/grants/installed_client",
        )), )
    def POST_access_token(self, grant_type):
        """
        Exchange an [OAuth 2.0](http://oauth.net/2/) authorization code
        or refresh token (from [/api/v1/authorize](#api_method_authorize)) for
        an access token.

        On success, returns a URL-encoded dictionary containing
        **access_token**, **token_type**, **expires_in**, and **scope**.
        If an authorization code for a permanent grant was given, a
        **refresh_token** will be included. If there is a problem, an **error**
        parameter will be returned instead.

        Must be called using SSL, and must contain a HTTP `Authorization:`
        header which contains the application's client identifier as the
        username and client secret as the password.  (The client id and secret
        are visible on the [app preferences page](/prefs/apps).)

        Per the OAuth specification, **grant_type** must be one of:

        * ``authorization_code`` for the initial access token ("standard" OAuth2 flow)
        * ``refresh_token`` for renewing the access token.
        * ``password`` for script-type apps using password auth
        * ``client_credentials`` for application-only (signed out) access - confidential clients
        * ``https://oauth.reddit.com/grants/installed_client`` extension grant for application-only (signed out)
                access - non-confidential (installed) clients

        **redirect_uri** must exactly match the value that was used in the call
        to [/api/v1/authorize](#api_method_authorize) that created this grant.

        See reddit's [OAuth2 wiki](https://github.com/reddit/reddit/wiki/OAuth2) for
        more information.

        """
        if grant_type == "authorization_code":
            return self._access_token_code()
        elif grant_type == "refresh_token":
            return self._access_token_refresh()
        elif grant_type == "password":
            return self._access_token_password()
        elif grant_type == "client_credentials":
            return self._access_token_client_credentials()
        elif grant_type == "https://oauth.reddit.com/grants/installed_client":
            return self._access_token_extension_client_credentials()
        else:
            resp = {"error": "unsupported_grant_type"}
            return self.api_wrapper(resp)

    def _check_for_errors(self):
        resp = {}
        if (errors.INVALID_OPTION, "scope") in c.errors:
            resp["error"] = "invalid_scope"
        else:
            resp["error"] = "invalid_request"
        return resp

    @classmethod
    def _make_token_dict(cls, access_token, refresh_token=None):
        if not access_token:
            return {"error": "invalid_grant"}
        expires_in = int(access_token._ttl) if access_token._ttl else None
        resp = {
            "access_token": access_token._id,
            "token_type": access_token.token_type,
            "expires_in": expires_in,
            "scope": access_token.scope,
        }
        if refresh_token:
            resp["refresh_token"] = refresh_token._id
        return resp

    @validate(code=nop("code"),
              redirect_uri=VRequired("redirect_uri",
                                     errors.OAUTH2_INVALID_REDIRECT_URI))
    def _access_token_code(self, code, redirect_uri):
        if not code:
            c.errors.add("NO_TEXT", field="code")
        if c.errors:
            return self.api_wrapper(self._check_for_errors())

        access_token = None
        refresh_token = None

        auth_token = OAuth2AuthorizationCode.use_token(code,
                                                       c.oauth2_client._id,
                                                       redirect_uri)
        if auth_token:
            if auth_token.refreshable:
                refresh_token = OAuth2RefreshToken._new(
                    auth_token.client_id, auth_token.user_id, auth_token.scope)
            access_token = OAuth2AccessToken._new(
                auth_token.client_id, auth_token.user_id, auth_token.scope,
                refresh_token._id if refresh_token else "")

        resp = self._make_token_dict(access_token, refresh_token)

        return self.api_wrapper(resp)

    @validate(refresh_token=VOAuth2RefreshToken("refresh_token"))
    def _access_token_refresh(self, refresh_token):
        access_token = None
        if refresh_token:
            if refresh_token.client_id == c.oauth2_client._id:
                access_token = OAuth2AccessToken._new(
                    refresh_token.client_id,
                    refresh_token.user_id,
                    refresh_token.scope,
                    refresh_token=refresh_token._id)
            else:
                c.errors.add(errors.OAUTH2_INVALID_REFRESH_TOKEN)
        else:
            c.errors.add("NO_TEXT", field="refresh_token")

        if c.errors:
            resp = self._check_for_errors()
            response.status = 400
        else:
            resp = self._make_token_dict(access_token)
        return self.api_wrapper(resp)

    @validate(user=VThrottledLogin(["username", "password"]),
              scope=nop("scope"))
    def _access_token_password(self, user, scope):
        # username:password auth via OAuth is only allowed for
        # private use scripts
        client = c.oauth2_client
        if client.app_type != "script":
            return self.api_wrapper({
                "error":
                "unauthorized_client",
                "error_description":
                "Only script apps may use password auth"
            })
        dev_ids = client._developer_ids
        if not user or user._id not in dev_ids:
            return self.api_wrapper({"error": "invalid_grant"})
        if c.errors:
            return self.api_wrapper(self._check_for_errors())

        if scope:
            scope = OAuth2Scope(scope)
            if not scope.is_valid():
                c.errors.add(errors.INVALID_OPTION, "scope")
                return self.api_wrapper({"error": "invalid_scope"})
        else:
            scope = OAuth2Scope(OAuth2Scope.FULL_ACCESS)

        access_token = OAuth2AccessToken._new(client._id, user._id36, scope)
        resp = self._make_token_dict(access_token)
        return self.api_wrapper(resp)

    @validate(
        scope=nop("scope"), )
    def _access_token_client_credentials(self, scope):
        client = c.oauth2_client
        if not client.is_confidential():
            return self.api_wrapper({
                "error":
                "unauthorized_client",
                "error_description":
                "Only confidential clients may use client_credentials auth"
            })
        if scope:
            scope = OAuth2Scope(scope)
            if not scope.is_valid():
                c.errors.add(errors.INVALID_OPTION, "scope")
                return self.api_wrapper({"error": "invalid_scope"})
        else:
            scope = OAuth2Scope(OAuth2Scope.FULL_ACCESS)

        access_token = OAuth2AccessToken._new(
            client._id,
            "",
            scope,
        )
        resp = self._make_token_dict(access_token)
        return self.api_wrapper(resp)

    @validate(
        scope=nop("scope"),
        device_id=VLength("device_id", 50, min_length=20),
    )
    def _access_token_extension_client_credentials(self, scope, device_id):
        if ((errors.NO_TEXT, "device_id") in c.errors
                or (errors.TOO_SHORT, "device_id") in c.errors
                or (errors.TOO_LONG, "device_id") in c.errors):
            return self.api_wrapper({
                "error": "invalid_request",
                "error_description": "bad device_id",
            })

        client = c.oauth2_client
        if scope:
            scope = OAuth2Scope(scope)
            if not scope.is_valid():
                c.errors.add(errors.INVALID_OPTION, "scope")
                return self.api_wrapper({"error": "invalid_scope"})
        else:
            scope = OAuth2Scope(OAuth2Scope.FULL_ACCESS)

        access_token = OAuth2AccessToken._new(
            client._id,
            "",
            scope,
            device_id=device_id,
        )
        resp = self._make_token_dict(access_token)
        return self.api_wrapper(resp)

    @validate(
        VRatelimit(rate_user=False, rate_ip=True, prefix="rate_revoke_token_"),
        token_id=nop("token"),
        token_hint=VOneOf("token_type_hint",
                          ("access_token", "refresh_token")),
    )
    def POST_revoke_token(self, token_id, token_hint):
        '''Revoke an OAuth2 access or refresh token.

        token_type_hint is optional, and hints to the server
        whether the passed token is a refresh or access token.

        A call to this endpoint is considered a success if
        the passed `token_id` is no longer valid. Thus, if an invalid
        `token_id` was passed in, a successful 204 response will be returned.

        See [RFC7009](http://tools.ietf.org/html/rfc7009)

        '''
        # In success cases, this endpoint returns no data.
        response.status = 204

        if not token_id:
            return

        types = (OAuth2AccessToken, OAuth2RefreshToken)
        if token_hint == "refresh_token":
            types = reversed(types)

        for token_type in types:
            try:
                token = token_type._byID(token_id)
            except tdb_cassandra.NotFound:
                continue
            else:
                break
        else:
            # No Token found. The given token ID is already gone
            # or never existed. Either way, from the client's perspective,
            # the passed in token is no longer valid.
            return

        if constant_time_compare(token.client_id, c.oauth2_client._id):
            token.revoke()
        else:
            # RFC 7009 is not clear on how to handle this case.
            # Given that a malicious client could do much worse things
            # with a valid token then revoke it, returning an error
            # here is best as it may help certain clients debug issues
            response.status = 400
            return self.api_wrapper({"error": "unauthorized_client"})
Beispiel #4
0
class OAuth2FrontendController(RedditController):
    def check_for_bearer_token(self):
        pass

    def pre(self):
        RedditController.pre(self)
        require_https()

    def _abort_oauth_error(self, error):
        g.stats.simple_event('oauth2.errors.%s' % error)
        abort(BadRequestError(error))

    def _check_redirect_uri(self, client, redirect_uri):
        if (errors.OAUTH2_INVALID_CLIENT, 'client_id') in c.errors:
            self._abort_oauth_error(errors.OAUTH2_INVALID_CLIENT)

        if not redirect_uri or redirect_uri != client.redirect_uri:
            self._abort_oauth_error(errors.OAUTH2_INVALID_REDIRECT_URI)

    def _check_response_type_and_scope(self, response_type, scope):
        if (errors.INVALID_OPTION, 'response_type') in c.errors:
            self._abort_oauth_error(errors.OAUTH2_INVALID_RESPONSE_TYPE)

        if (errors.OAUTH2_INVALID_SCOPE, 'scope') in c.errors:
            self._abort_oauth_error(errors.OAUTH2_INVALID_SCOPE)

    def _check_client_type_and_duration(self, response_type, client, duration):
        if response_type == "token" and client.is_confidential():
            # Prevent "confidential" clients from distributing tokens
            # in a non-confidential manner
            self._abort_oauth_error(errors.OAUTH2_CONFIDENTIAL_TOKEN)

        if response_type == "token" and duration != "temporary":
            # implicit grant -> No refresh tokens allowed
            self._abort_oauth_error(errors.OAUTH2_NO_REFRESH_TOKENS_ALLOWED)

    def _error_response(self, state, redirect_uri, as_fragment=False):
        """Return an error redirect."""
        resp = {"state": state}

        if (errors.OAUTH2_ACCESS_DENIED, "authorize") in c.errors:
            resp["error"] = "access_denied"
        elif (errors.INVALID_MODHASH, None) in c.errors:
            resp["error"] = "access_denied"
        else:
            resp["error"] = "invalid_request"

        final_redirect = _update_redirect_uri(redirect_uri, resp, as_fragment)
        return self.redirect(final_redirect, code=302)

    def _check_employee_grants(self, client, scope):
        if not c.user.employee or not client or not scope:
            return
        if client._id in g.employee_approved_clients:
            return
        if client._id in g.mobile_auth_allowed_clients:
            return
        # The identity scope doesn't leak much, and we don't mind if employees
        # prove their identity to some external service
        if scope.scopes == {"identity"}:
            return
        error_page = RedditError(
            title=_('this app has not been approved for use with employee accounts'),
            message="",
        )
        request.environ["usable_error_content"] = error_page.render()
        self.abort403()

    @validate(VUser(),
              response_type = VOneOf("response_type", ("code", "token")),
              client = VOAuth2ClientID(),
              redirect_uri = VRequired("redirect_uri", errors.OAUTH2_INVALID_REDIRECT_URI),
              scope = VOAuth2Scope(),
              state = VRequired("state", errors.NO_TEXT),
              duration = VOneOf("duration", ("temporary", "permanent"),
                                default="temporary"))
    def GET_authorize(self, response_type, client, redirect_uri, scope, state,
                      duration):
        """
        First step in [OAuth 2.0](http://oauth.net/2/) authentication.
        End users will be prompted for their credentials (username/password)
        and asked if they wish to authorize the application identified by
        the **client_id** parameter with the permissions specified by the
        **scope** parameter.  They are then redirected to the endpoint on
        the client application's side specified by **redirect_uri**.

        If the user granted permission to the application, the response will
        contain a **code** parameter with a temporary authorization code
        which can be exchanged for an access token at
        [/api/v1/access_token](#api_method_access_token).

        **redirect_uri** must match the URI configured for the client in the
        [app preferences](/prefs/apps).  All errors will show a 400 error
        page along with some information on what option was wrong.
        """
        self._check_employee_grants(client, scope)

        # Check redirect URI first; it will ensure client exists
        self._check_redirect_uri(client, redirect_uri)

        self._check_response_type_and_scope(response_type, scope)

        self._check_client_type_and_duration(response_type, client, duration)

        if not c.errors:
            return OAuth2AuthorizationPage(client, redirect_uri, scope, state,
                                           duration, response_type).render()
        else:
            self._abort_oauth_error(errors.INVALID_OPTION)

    @validate(VUser(),
              VModhash(fatal=False),
              client = VOAuth2ClientID(),
              redirect_uri = VRequired("redirect_uri", errors.OAUTH2_INVALID_REDIRECT_URI),
              scope = VOAuth2Scope(),
              state = VRequired("state", errors.NO_TEXT),
              duration = VOneOf("duration", ("temporary", "permanent"),
                                default="temporary"),
              authorize = VRequired("authorize", errors.OAUTH2_ACCESS_DENIED),
              response_type = VOneOf("response_type", ("code", "token"),
                                     default="code"))
    def POST_authorize(self, authorize, client, redirect_uri, scope, state,
                       duration, response_type):
        """Endpoint for OAuth2 authorization."""

        self._check_employee_grants(client, scope)

        self._check_redirect_uri(client, redirect_uri)

        self._check_response_type_and_scope(response_type, scope)

        self._check_client_type_and_duration(response_type, client, duration)

        if c.errors:
            return self._error_response(state, redirect_uri,
                                        as_fragment=(response_type == "token"))

        if response_type == "code":
            code = OAuth2AuthorizationCode._new(client._id, redirect_uri,
                                            c.user._id36, scope,
                                            duration == "permanent")
            resp = {"code": code._id, "state": state}
            final_redirect = _update_redirect_uri(redirect_uri, resp)
            g.stats.simple_event('oauth2.POST_authorize.authorization_code_create')
        elif response_type == "token":
            device_id = get_device_id(client)
            token = OAuth2AccessToken._new(
                client_id=client._id,
                user_id=c.user._id36,
                scope=scope,
                device_id=device_id,
            )
            resp = OAuth2AccessController._make_new_token_response(token)
            resp["state"] = state
            final_redirect = _update_redirect_uri(redirect_uri, resp, as_fragment=True)
            g.stats.simple_event('oauth2.POST_authorize.access_token_create')

        # If this is the first time the user is logging in with an official
        # mobile app, gild them
        if (g.live_config.get('mobile_gild_first_login') and
                not c.user.has_used_mobile_app and
                client._id in g.mobile_auth_gild_clients):
            buyer = Account.system_user()
            admintools.adjust_gold_expiration(
                c.user, days=g.mobile_auth_gild_time)
            create_gift_gold(
                buyer._id, c.user._id, g.mobile_auth_gild_time,
                datetime.now(g.tz), signed=True, note='first_mobile_auth')
            subject = 'Let there be gold! Reddit just sent you Reddit gold!'
            message = (
                "Thank you for using the Reddit mobile app!  As a thank you "
                "for logging in during launch week, you've been gifted %s of "
                "Reddit Gold.\n\n"
                "Reddit Gold is Reddit's premium membership program, which "
                "grants you: \n"
                "An ads-free experience in Reddit's mobile apps, and\n"
                "Extra site features on desktop\n\n"
                "Discuss and get help on the features and perks at "
                "r/goldbenefits."
            ) % g.mobile_auth_gild_message
            message += '\n\n' + strings.gold_benefits_msg
            send_system_message(c.user, subject, message, add_to_sent=False)
            c.user.has_used_mobile_app = True
            c.user._commit()

        return self.redirect(final_redirect, code=302)
Beispiel #5
0
class OAuth2AccessController(MinimalController):
    def pre(self):
        set_extension(request.environ, "json")
        MinimalController.pre(self)
        require_https()
        c.oauth2_client = self._get_client_auth()

    def _get_client_auth(self):
        auth = request.headers.get("Authorization")
        try:
            client_id, client_secret = parse_http_basic(auth)
            client = OAuth2Client.get_token(client_id)
            require(client)
            require(constant_time_compare(client.secret, client_secret))
            return client
        except RequirementException:
            abort(401, headers=[("WWW-Authenticate", 'Basic realm="reddit"')])

    @validate(grant_type=VOneOf(
        "grant_type", ("authorization_code", "refresh_token", "password")))
    def POST_access_token(self, grant_type):
        """
        Exchange an [OAuth 2.0](http://oauth.net/2/) authorization code
        or refresh token (from [/api/v1/authorize](#api_method_authorize)) for
        an access token.

        On success, returns a URL-encoded dictionary containing
        **access_token**, **token_type**, **expires_in**, and **scope**.
        If an authorization code for a permanent grant was given, a
        **refresh_token** will be included. If there is a problem, an **error**
        parameter will be returned instead.

        Must be called using SSL, and must contain a HTTP `Authorization:`
        header which contains the application's client identifier as the
        username and client secret as the password.  (The client id and secret
        are visible on the [app preferences page](/prefs/apps).)

        Per the OAuth specification, **grant_type** must
        be ``authorization_code`` for the initial access token or
        ``refresh_token`` for renewing the access token.

        **redirect_uri** must exactly match the value that was used in the call
        to [/api/v1/authorize](#api_method_authorize) that created this grant.
        """

        if grant_type == "authorization_code":
            return self._access_token_code()
        elif grant_type == "refresh_token":
            return self._access_token_refresh()
        elif grant_type == "password":
            return self._access_token_password()
        else:
            resp = {"error": "unsupported_grant_type"}
            return self.api_wrapper(resp)

    def _check_for_errors(self):
        resp = {}
        if (errors.INVALID_OPTION, "scope") in c.errors:
            resp["error"] = "invalid_scope"
        else:
            resp["error"] = "invalid_request"
        return resp

    def _make_token_dict(self, access_token, refresh_token=None):
        if not access_token:
            return {"error": "invalid_grant"}
        expires_in = int(access_token._ttl) if access_token._ttl else None
        resp = {
            "access_token": access_token._id,
            "token_type": access_token.token_type,
            "expires_in": expires_in,
            "scope": access_token.scope,
        }
        if refresh_token:
            resp["refresh_token"] = refresh_token._id
        return resp

    @validate(code=nop("code"),
              redirect_uri=VRequired("redirect_uri",
                                     errors.OAUTH2_INVALID_REDIRECT_URI))
    def _access_token_code(self, code, redirect_uri):
        if not code:
            c.errors.add("NO_TEXT", field="code")
        if c.errors:
            return self.api_wrapper(self._check_for_errors())

        access_token = None
        refresh_token = None

        auth_token = OAuth2AuthorizationCode.use_token(code,
                                                       c.oauth2_client._id,
                                                       redirect_uri)
        if auth_token:
            if auth_token.refreshable:
                refresh_token = OAuth2RefreshToken._new(
                    auth_token.client_id, auth_token.user_id, auth_token.scope)
            access_token = OAuth2AccessToken._new(
                auth_token.client_id, auth_token.user_id, auth_token.scope,
                refresh_token._id if refresh_token else None)

        resp = self._make_token_dict(access_token, refresh_token)

        return self.api_wrapper(resp)

    @validate(refresh_token=VOAuth2RefreshToken("refresh_token"))
    def _access_token_refresh(self, refresh_token):
        resp = {}
        access_token = None
        if refresh_token:
            access_token = OAuth2AccessToken._new(
                refresh_token.client_id,
                refresh_token.user_id,
                refresh_token.scope,
                refresh_token=refresh_token._id)
        else:
            c.errors.add("NO_TEXT", field="refresh_token")

        if c.errors:
            resp = self._check_for_errors()
        else:
            resp = self._make_token_dict(access_token)
        return self.api_wrapper(resp)

    @validate(user=VThrottledLogin(["username", "password"]),
              scope=nop("scope"))
    def _access_token_password(self, user, scope):
        # username:password auth via OAuth is only allowed for
        # private use scripts
        client = c.oauth2_client
        if client.app_type != "script":
            return self.api_wrapper({
                "error":
                "unauthorized_client",
                "error_description":
                "Only script apps may use password auth"
            })
        dev_ids = client._developer_ids
        if not user or user._id not in dev_ids:
            return self.api_wrapper({"error": "invalid_grant"})
        if c.errors:
            return self.api_wrapper(self._check_for_errors())

        if scope:
            scope = OAuth2Scope(scope)
            if not scope.is_valid():
                c.errors.add(errors.INVALID_OPTION, "scope")
                return self.api_wrapper({"error": "invalid_scope"})
        else:
            scope = OAuth2Scope(OAuth2Scope.FULL_ACCESS)

        access_token = OAuth2AccessToken._new(client._id, user._id36, scope)
        resp = self._make_token_dict(access_token)
        return self.api_wrapper(resp)
Beispiel #6
0
class OAuth2AccessController(MinimalController):
    def pre(self):
        set_extension(request.environ, "json")
        MinimalController.pre(self)
        require_https()
        c.oauth2_client = self._get_client_auth()

    def _get_client_auth(self):
        auth = request.headers.get("Authorization")
        try:
            client_id, client_secret = parse_http_basic(auth)
            client = OAuth2Client.get_token(client_id)
            require(client)
            require(constant_time_compare(client.secret, client_secret))
            return client
        except RequirementException:
            abort(401, headers=[("WWW-Authenticate", 'Basic realm="reddit"')])

    @validate(grant_type=VOneOf("grant_type",
                                ("authorization_code", "refresh_token")),
              code=nop("code"),
              refresh_token=VOAuth2RefreshToken("refresh_token"),
              redirect_uri=VRequired("redirect_uri",
                                     errors.OAUTH2_INVALID_REDIRECT_URI))
    def POST_access_token(self, grant_type, code, refresh_token, redirect_uri):
        """
        Exchange an [OAuth 2.0](http://oauth.net/2/) authorization code
        or refresh token (from [/api/v1/authorize](#api_method_authorize)) for
        an access token.

        On success, returns a URL-encoded dictionary containing
        **access_token**, **token_type**, **expires_in**, and **scope**.
        If an authorization code for a permanent grant was given, a
        **refresh_token** will be included. If there is a problem, an **error**
        parameter will be returned instead.

        Must be called using SSL, and must contain a HTTP `Authorization:`
        header which contains the application's client identifier as the
        username and client secret as the password.  (The client id and secret
        are visible on the [app preferences page](/prefs/apps).)

        Per the OAuth specification, **grant_type** must
        be ``authorization_code`` for the initial access token or
        ``refresh_token`` for renewing the access token. In either case,
        **redirect_uri** must exactly match the value that was used in the call
        to [/api/v1/authorize](#api_method_authorize) that created this grant.
        """

        resp = {}
        if not (code or refresh_token):
            c.errors.add("NO_TEXT", field=("code", "refresh_token"))
        if not c.errors:
            access_token = None

            if grant_type == "authorization_code":
                auth_token = OAuth2AuthorizationCode.use_token(
                    code, c.oauth2_client._id, redirect_uri)
                if auth_token:
                    if auth_token.refreshable:
                        refresh_token = OAuth2RefreshToken._new(
                            auth_token.client_id, auth_token.user_id,
                            auth_token.scope)
                    access_token = OAuth2AccessToken._new(
                        auth_token.client_id, auth_token.user_id,
                        auth_token.scope,
                        refresh_token._id if refresh_token else None)
            elif grant_type == "refresh_token" and refresh_token:
                access_token = OAuth2AccessToken._new(
                    refresh_token.client_id,
                    refresh_token.user_id,
                    refresh_token.scope,
                    refresh_token=refresh_token._id)

            if access_token:
                resp["access_token"] = access_token._id
                resp["token_type"] = access_token.token_type
                resp["expires_in"] = int(
                    access_token._ttl) if access_token._ttl else None
                resp["scope"] = access_token.scope
                if refresh_token:
                    resp["refresh_token"] = refresh_token._id
            else:
                resp["error"] = "invalid_grant"
        else:
            if (errors.INVALID_OPTION, "grant_type") in c.errors:
                resp["error"] = "unsupported_grant_type"
            elif (errors.INVALID_OPTION, "scope") in c.errors:
                resp["error"] = "invalid_scope"
            else:
                resp["error"] = "invalid_request"

        return self.api_wrapper(resp)